diff --git a/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json b/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json new file mode 100644 index 00000000000..3a97907cb1d --- /dev/null +++ b/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "Support full Unicode range in inline chat panel on Windows" +} \ No newline at end of file diff --git a/.changes/next-release/feature-f8a047df-70ed-4c42-b9f7-f3b97e9c9a6e.json b/.changes/next-release/feature-f8a047df-70ed-4c42-b9f7-f3b97e9c9a6e.json new file mode 100644 index 00000000000..e1fcfbfe7da --- /dev/null +++ b/.changes/next-release/feature-f8a047df-70ed-4c42-b9f7-f3b97e9c9a6e.json @@ -0,0 +1,4 @@ +{ + "type" : "feature", + "description" : "Agentic coding experience: Amazon Q can now write code and run shell commands on your behalf" +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/temp-toolkit-intellij-root-conventions.gradle.kts b/buildSrc/src/main/kotlin/temp-toolkit-intellij-root-conventions.gradle.kts index c98014b4e91..d77ad7ffa69 100644 --- a/buildSrc/src/main/kotlin/temp-toolkit-intellij-root-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/temp-toolkit-intellij-root-conventions.gradle.kts @@ -35,7 +35,7 @@ val toolkitVersion: String by project // please check changelog generation logic if this format is changed // also sync with gateway version -version = "$toolkitVersion-${ideProfile.shortName}" +version = "$toolkitVersion.${ideProfile.shortName}" val resharperDlls = configurations.register("resharperDlls") { isCanBeConsumed = false diff --git a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts index 3d02f8cd7f1..f39c251dbeb 100644 --- a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts @@ -16,7 +16,7 @@ val ideProfile = IdeVersions.ideProfile(project) val toolkitVersion: String by project // please check changelog generation logic if this format is changed -version = "$toolkitVersion-${ideProfile.shortName}" +version = "$toolkitVersion.${ideProfile.shortName}" // attach the current commit hash on local builds if (!project.isCi()) { diff --git a/plugins/amazonq/build.gradle.kts b/plugins/amazonq/build.gradle.kts index b20472a83f7..39bbdd368fa 100644 --- a/plugins/amazonq/build.gradle.kts +++ b/plugins/amazonq/build.gradle.kts @@ -1,17 +1,24 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import de.undercouch.gradle.tasks.download.Download +import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask import software.aws.toolkits.gradle.changelog.tasks.GeneratePluginChangeLog -import software.aws.toolkits.gradle.intellij.IdeFlavor -import software.aws.toolkits.gradle.intellij.IdeVersions -import software.aws.toolkits.gradle.intellij.toolkitIntelliJ plugins { id("toolkit-publishing-conventions") id("toolkit-publish-root-conventions") id("toolkit-jvm-conventions") id("toolkit-testing") + id("de.undercouch.download") +} + +buildscript { + dependencies { + classpath(libs.bundles.jackson) + } } val changelog = tasks.register("pluginChangeLog") { @@ -51,3 +58,81 @@ tasks.check { } } } + +val downloadFlareManifest by tasks.registering(Download::class) { + src("https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json") + dest(layout.buildDirectory.file("flare/manifest.json")) + onlyIfModified(true) + useETag(true) +} + +data class FlareManifest( + val versions: List, +) + +data class FlareVersion( + val serverVersion: String, + val thirdPartyLicenses: String, + val targets: List, +) + +data class FlareTarget( + val platform: String, + val arch: String, + val contents: List +) + +data class FlareContent( + val url: String, +) + +val downloadFlareArtifacts by tasks.registering(Download::class) { + dependsOn(downloadFlareManifest) + inputs.files(downloadFlareManifest) + + val manifestFile = downloadFlareManifest.map { it.outputFiles.first() } + val manifest = manifestFile.map { jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(it.readText(), FlareManifest::class.java) } + + // use darwin-aarch64 because its the smallest and we're going to throw away everything platform specific + val latest = manifest.map { it.versions.first() } + val latestVersion = latest.map { it.serverVersion } + val licensesUrl = latest.map { it.thirdPartyLicenses } + val darwin = latest.map { it.targets.first { target -> target.platform == "darwin" && target.arch == "arm64" } } + val contentUrls = darwin.map { it.contents.map { content -> content.url } } + + val destination = layout.buildDirectory.dir(latestVersion.map { "flare/$it" }) + outputs.dir(destination) + + src(contentUrls.zip(licensesUrl) { left, right -> left + right}) + dest(destination) + onlyIfModified(true) + useETag(true) +} + +val prepareBundledFlare by tasks.registering(Copy::class) { + dependsOn(downloadFlareArtifacts) + inputs.files(downloadFlareArtifacts) + + val dest = layout.buildDirectory.dir("tmp/extractFlare") + into(dest) + from(downloadFlareArtifacts.map { it.outputFiles.filterNot { file -> file.name.endsWith(".zip") } }) + + doLast { + copy { + into(dest) + includeEmptyDirs = false + downloadFlareArtifacts.get().outputFiles.filter { it.name.endsWith(".zip") }.forEach { + dest.get().file(it.parentFile.name).asFile.createNewFile() + from(zipTree(it)) { + include("*.js") + include("*.txt") + } + } + } + } +} + +tasks.withType().configureEach { + intoChild(intellijPlatform.projectName.map { "$it/flare" }) + .from(prepareBundledFlare) +} diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index 6949f4ba397..5d4e24f84be 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -8,8 +8,6 @@ - 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 3650491688f..cee580575ef 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 @@ -18,6 +18,10 @@ 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.cef.CefApp import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.error @@ -40,6 +44,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIn 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.services.amazonq.webview.theme.EditorThemeAdapter +import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter import software.aws.toolkits.jetbrains.utils.isQConnected import software.aws.toolkits.jetbrains.utils.isQExpired import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable @@ -54,7 +60,7 @@ import javax.swing.JButton import javax.swing.JComponent @Service(Service.Level.PROJECT) -class QWebviewPanel private constructor(val project: Project) : Disposable { +class QWebviewPanel private constructor(val project: Project, private val scope: CoroutineScope) : Disposable { private val webviewContainer = Wrapper() var browser: QWebviewBrowser? = null private set @@ -102,6 +108,14 @@ class QWebviewPanel private constructor(val project: Project) : Disposable { } else { browser = QWebviewBrowser(project, this).also { webviewContainer.add(it.component()) + + val themeBrowserAdapter = ThemeBrowserAdapter() + EditorThemeAdapter().onThemeChange() + .distinctUntilChanged() + .onEach { theme -> + themeBrowserAdapter.updateLoginThemeInBrowser(it.jcefBrowser.cefBrowser, theme) + } + .launchIn(scope) } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt index 8b22e156399..84ba01c0e4c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt @@ -11,7 +11,6 @@ import com.intellij.openapi.project.DumbAwareAction import com.intellij.util.messages.Topic import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow import software.aws.toolkits.resources.AmazonQBundle -import software.aws.toolkits.resources.message import java.util.EventListener class QRefreshPanelAction : DumbAwareAction(AmazonQBundle.message("amazonq.refresh.panel"), null, AllIcons.Actions.Refresh) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt index 726ba1fb34c..29b7213a76b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.treeToValue import org.jetbrains.annotations.VisibleForTesting import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.UnknownMessageType @@ -19,7 +20,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.util.command class MessageSerializer @VisibleForTesting constructor() { - private val objectMapper = jacksonObjectMapper() + val objectMapper = jacksonObjectMapper() .registerModule(JavaTimeModule()) .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) @@ -36,6 +37,12 @@ class MessageSerializer @VisibleForTesting constructor() { fun serialize(value: Any): String = objectMapper.writeValueAsString(value) + inline fun deserializeChatMessages(value: JsonNode): T = + objectMapper.treeToValue(value) + + inline fun deserializeChatMessages(value: JsonNode, clazz: Class): T = + objectMapper.treeToValue(value, clazz) + // Provide singleton global access companion object { private val instance = MessageSerializer() 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 b85d94db10f..0f4aca90238 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 @@ -6,14 +6,8 @@ package software.aws.toolkits.jetbrains.services.amazonq.startup import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project -import com.intellij.openapi.project.waitForSmartMode import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.wm.ToolWindowManager -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.delay -import kotlinx.coroutines.time.withTimeout -import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.warn 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 @@ -21,14 +15,11 @@ 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 import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings -import java.lang.management.ManagementFactory -import java.time.Duration import java.util.concurrent.atomic.AtomicBoolean class AmazonQStartupActivity : ProjectActivity { @@ -58,39 +49,8 @@ class AmazonQStartupActivity : ProjectActivity { QRegionProfileManager.getInstance().validateProfile(project) AmazonQLspService.getInstance(project) - startLsp(project) if (runOnce.get()) return emitUserState(project) runOnce.set(true) } - - private suspend fun startLsp(project: Project) { - // Automatically start the project context LSP after some delay when average CPU load is below 30%. - // The CPU load requirement is to avoid competing with native JetBrains indexing and other CPU expensive OS processes - // In the future we will decouple LSP start and indexing start to let LSP perform other tasks. - val startLspIndexingDuration = Duration.ofMinutes(30) - project.waitForSmartMode() - delay(30_000) // Wait for 30 seconds for systemLoadAverage to be more accurate - try { - withTimeout(startLspIndexingDuration) { - while (true) { - val cpuUsage = ManagementFactory.getOperatingSystemMXBean().systemLoadAverage - if (cpuUsage > 0 && cpuUsage < 30) { - ProjectContextController.getInstance(project = project) - break - } else { - delay(60_000) // Wait for 60 seconds - } - } - } - } catch (e: TimeoutCancellationException) { - LOG.warn { "Failed to start LSP server due to time out" } - } catch (e: Exception) { - LOG.warn { "Failed to start LSP server" } - } - } - - companion object { - private val LOG = getLogger() - } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt index 396057bc9a9..53323b638b2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt @@ -5,7 +5,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.toolwindow import com.intellij.idea.AppMode import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBLoadingPanel import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.dsl.builder.Align @@ -13,15 +16,59 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.AlignY import com.intellij.ui.dsl.builder.panel import com.intellij.ui.jcef.JBCefApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.isDeveloperMode +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection +import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry +import software.aws.toolkits.jetbrains.services.amazonq.isQSupportedInThisVersion +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage +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.profile.QRegionProfileManager +import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser -import java.awt.event.ActionListener +import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector +import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter +import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter +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 +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable +import software.aws.toolkits.resources.message +import java.util.concurrent.CompletableFuture import javax.swing.JButton -class AmazonQPanel(private val parent: Disposable) { +class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Disposable { + private val browser = CompletableFuture() private val webviewContainer = Wrapper() - var browser: Browser? = null - private set + private val appSource = AppSource() + private val browserConnector = BrowserConnector(project = project) + private val editorThemeAdapter = EditorThemeAdapter() + private val appConnections = mutableListOf() + + init { + project.messageBus.connect().subscribe( + AsyncChatUiListener.TOPIC, + object : AsyncChatUiListener { + override fun onChange(command: String) { + browser.get()?.postChat(command) + } + + override fun onChange(command: FlareUiMessage) { + browser.get()?.postChat(command) + } + } + ) + } val component = panel { row { @@ -34,14 +81,12 @@ class AmazonQPanel(private val parent: Disposable) { row { cell( JButton("Show Web Debugger").apply { - addActionListener( - ActionListener { - // Code to be executed when the button is clicked - // Add your logic here - - browser?.jcefBrowser?.openDevtools() - }, - ) + addActionListener { + // Code to be executed when the button is clicked + // Add your logic here + + browser.get().jcefBrowser.openDevtools() + } }, ) .align(AlignX.CENTER) @@ -51,19 +96,6 @@ class AmazonQPanel(private val parent: Disposable) { } init { - init() - } - - fun disposeAndRecreate() { - webviewContainer.removeAll() - val toDispose = browser - init() - if (toDispose != null) { - Disposer.dispose(toDispose) - } - } - - private fun init() { if (!JBCefApp.isSupported()) { // Fallback to an alternative browser-less solution if (AppMode.isRemoteDevHost()) { @@ -71,11 +103,114 @@ class AmazonQPanel(private val parent: Disposable) { } else { webviewContainer.add(JBTextArea("JCEF not supported")) } - browser = null + browser.complete(null) + } else if (!isQSupportedInThisVersion()) { + webviewContainer.add(JBTextArea("${message("q.unavailable")}\n ${message("q.unavailable.node")}")) + browser.complete(null) } else { - browser = Browser(parent).also { - webviewContainer.add(it.component()) + val loadingPanel = JBLoadingPanel(null, this) + val wrapper = Wrapper() + loadingPanel.startLoading() + + webviewContainer.add(wrapper) + wrapper.setContent(loadingPanel) + + scope.launch { + val webUri = service().fetchArtifact(project).resolve("amazonq-ui.js").toUri() + // wait for server to be running + AmazonQLspService.getInstance(project).instanceFlow.first() + + withContext(EDT) { + browser.complete( + Browser(this@AmazonQPanel, webUri, project).also { + wrapper.setContent(it.component()) + + initConnections() + connectUi(it) + connectApps(it) + + loadingPanel.stopLoading() + } + ) + } + } + } + } + + fun sendMessage(message: AmazonQMessage, tabType: String) { + appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach { + scope.launch { + it.messagesFromUiToApp.publish(message) } } } + + fun sendMessageAppToUi(message: AmazonQMessage, tabType: String) { + appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach { + scope.launch { + it.messagesFromAppToUi.publish(message) + } + } + } + + private fun initConnections() { + val apps = appSource.getApps(project) + apps.forEach { app -> + appConnections += AppConnection( + app = app, + messagesFromAppToUi = MessageConnector(), + messagesFromUiToApp = MessageConnector(), + messageTypeRegistry = MessageTypeRegistry(), + ) + } + } + + private fun connectApps(browser: Browser) { + val fqnWebviewAdapter = FqnWebviewAdapter(browser.jcefBrowser, browserConnector) + + appConnections.forEach { connection -> + val initContext = AmazonQAppInitContext( + project = project, + messagesFromAppToUi = connection.messagesFromAppToUi, + messagesFromUiToApp = connection.messagesFromUiToApp, + messageTypeRegistry = connection.messageTypeRegistry, + fqnWebviewAdapter = fqnWebviewAdapter, + ) + // Connect the app to the UI + connection.app.init(initContext) + // Dispose of the app when the tool window is disposed. + Disposer.register(this, connection.app) + } + } + + private fun connectUi(browser: Browser) { + browser.init( + isCodeTransformAvailable = isCodeTransformAvailable(project), + isFeatureDevAvailable = isFeatureDevAvailable(project), + isCodeScanAvailable = isCodeScanAvailable(project), + isCodeTestAvailable = isCodeTestAvailable(project), + isDocAvailable = isDocAvailable(project), + highlightCommand = highlightCommand(), + activeProfile = QRegionProfileManager.getInstance().takeIf { it.shouldDisplayProfileInfo(project) }?.activeProfile(project) + ) + + scope.launch { + // Pipe messages from the UI to the relevant apps and vice versa + browserConnector.connect( + browser = browser, + connections = appConnections, + ) + } + + scope.launch { + // Update the theme in the UI when the IDE theme changes + browserConnector.connectTheme( + chatBrowser = browser.jcefBrowser.cefBrowser, + themeSource = editorThemeAdapter.onThemeChange(), + ) + } + } + + override fun dispose() { + } } 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 4c1191cd85d..5df972bba09 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 @@ -3,153 +3,30 @@ package software.aws.toolkits.jetbrains.services.amazonq.toolwindow -import com.intellij.ide.ui.LafManager -import com.intellij.ide.ui.LafManagerListener import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext -import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection -import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry -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 -import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter -import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable import software.aws.toolkits.jetbrains.services.amazonqCodeScan.runCodeScanMessage -import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable -import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable -import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable -import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable -import javax.swing.JComponent @Service(Service.Level.PROJECT) class AmazonQToolWindow private constructor( private val project: Project, private val scope: CoroutineScope, ) : Disposable { - private val appSource = AppSource() - private val browserConnector = BrowserConnector() - private val editorThemeAdapter = EditorThemeAdapter() - - private val chatPanel = AmazonQPanel(parent = this) - - val component: JComponent = chatPanel.component - - private val appConnections = mutableListOf() - - init { - initConnections() - connectUi() - connectApps() - } + private var chatPanel = AmazonQPanel(project, scope) + val component + get() = chatPanel.component fun disposeAndRecreate() { - browserConnector.uiReady = CompletableDeferred() - chatPanel.disposeAndRecreate() - - appConnections.clear() - initConnections() - connectUi() - connectApps() - - runInEdt { - ApplicationManager.getApplication().messageBus.syncPublisher(LafManagerListener.TOPIC).lookAndFeelChanged(LafManager.getInstance()) - } - } - - private fun sendMessage(message: AmazonQMessage, tabType: String) { - appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach { - scope.launch { - it.messagesFromUiToApp.publish(message) - } - } - } - - private fun sendMessageAppToUi(message: AmazonQMessage, tabType: String) { - appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach { - scope.launch { - it.messagesFromAppToUi.publish(message) - } - } - } - - private fun initConnections() { - val apps = appSource.getApps(project) - apps.forEach { app -> - appConnections += AppConnection( - app = app, - messagesFromAppToUi = MessageConnector(), - messagesFromUiToApp = MessageConnector(), - messageTypeRegistry = MessageTypeRegistry(), - ) - } - } - - private fun connectApps() { - val browser = chatPanel.browser ?: return - - val fqnWebviewAdapter = FqnWebviewAdapter(browser.jcefBrowser, browserConnector) - - appConnections.forEach { connection -> - val initContext = AmazonQAppInitContext( - project = project, - messagesFromAppToUi = connection.messagesFromAppToUi, - messagesFromUiToApp = connection.messagesFromUiToApp, - messageTypeRegistry = connection.messageTypeRegistry, - fqnWebviewAdapter = fqnWebviewAdapter, - ) - // Connect the app to the UI - connection.app.init(initContext) - // Dispose of the app when the tool window is disposed. - Disposer.register(this, connection.app) - } - } - - private fun connectUi() { - val chatBrowser = chatPanel.browser ?: return - val loginBrowser = QWebviewPanel.getInstance(project).browser ?: return - - chatBrowser.init( - isCodeTransformAvailable = isCodeTransformAvailable(project), - isFeatureDevAvailable = isFeatureDevAvailable(project), - isCodeScanAvailable = isCodeScanAvailable(project), - isCodeTestAvailable = isCodeTestAvailable(project), - isDocAvailable = isDocAvailable(project), - highlightCommand = highlightCommand(), - activeProfile = QRegionProfileManager.getInstance().takeIf { it.shouldDisplayProfileInfo(project) }?.activeProfile(project) - ) - - scope.launch { - // Pipe messages from the UI to the relevant apps and vice versa - browserConnector.connect( - browser = chatBrowser, - connections = appConnections, - ) - } - - scope.launch { - // Update the theme in the UI when the IDE theme changes - browserConnector.connectTheme( - chatBrowser = chatBrowser.jcefBrowser.cefBrowser, - loginBrowser = loginBrowser.jcefBrowser.cefBrowser, - themeSource = editorThemeAdapter.onThemeChange(), - ) - } + Disposer.dispose(chatPanel) + chatPanel = AmazonQPanel(project, scope) } companion object { @@ -166,13 +43,13 @@ class AmazonQToolWindow private constructor( // Send the interaction message val window = getInstance(project) - window.sendMessage(OnboardingPageInteraction(OnboardingPageInteractionType.CwcButtonClick), "cwc") + window.chatPanel.sendMessage(OnboardingPageInteraction(OnboardingPageInteractionType.CwcButtonClick), "cwc") } fun openScanTab(project: Project) { showChatWindow(project) val window = getInstance(project) - window.sendMessageAppToUi(runCodeScanMessage, tabType = "codescan") + window.chatPanel.sendMessageAppToUi(runCodeScanMessage, tabType = "codescan") } } 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 a61fa43855b..3ae16dd96c6 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 @@ -66,7 +66,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let { qConn -> openMeetQPage(project) } - prepareChatContent(project, qPanel) + preparePanelContent(project, qPanel) } } ) @@ -75,7 +75,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { RefreshQChatPanelButtonPressedListener.TOPIC, object : RefreshQChatPanelButtonPressedListener { override fun onRefresh() { - prepareChatContent(project, qPanel) + preparePanelContent(project, qPanel) } } ) @@ -85,8 +85,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { object : BearerTokenProviderListener { override fun onChange(providerId: String, newScopes: List?) { if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) { - AmazonQToolWindow.getInstance(project).disposeAndRecreate() - prepareChatContent(project, qPanel) + preparePanelContent(project, qPanel) } } } @@ -98,13 +97,12 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { // 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() - prepareChatContent(project, qPanel) + preparePanelContent(project, qPanel) } } ) - prepareChatContent(project, qPanel) + preparePanelContent(project, qPanel) val content = contentManager.factory.createContent(mainPanel, null, false).also { it.isCloseable = true @@ -114,7 +112,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { contentManager.addContent(content) } - private fun prepareChatContent( + private fun preparePanelContent( project: Project, qPanel: Wrapper, ) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt deleted file mode 100644 index 72524e49b27..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt +++ /dev/null @@ -1,39 +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.amazonq.toolwindow - -import com.intellij.openapi.project.Project -import com.intellij.ui.components.panels.Wrapper -import com.intellij.util.ui.components.BorderLayoutPanel -import software.aws.toolkits.core.utils.error -import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.jetbrains.core.webview.BrowserState -import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel -import software.aws.toolkits.jetbrains.utils.isQConnected -import software.aws.toolkits.jetbrains.utils.isQExpired -import software.aws.toolkits.telemetry.FeatureId -import javax.swing.JComponent - -class OuterAmazonQPanel(val project: Project) : BorderLayoutPanel() { - private val wrapper = Wrapper() - init { - isOpaque = false - addToCenter(wrapper) - val component = if (isQConnected(project) && !isQExpired(project)) { - AmazonQToolWindow.getInstance(project).component - } else { - QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ)) - QWebviewPanel.getInstance(project).component - } - updateQPanel(component) - } - - fun updateQPanel(content: JComponent) { - try { - wrapper.setContent(content) - } catch (e: Exception) { - getLogger().error { "Error while creating window" } - } - } -} 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 7dd41c795f1..e1d917f98a3 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 @@ -1,22 +1,29 @@ // Copyright 2023 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.webview import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.google.gson.Gson import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.ui.jcef.JBCefJSQuery import org.cef.CefApp +import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage 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 +import java.net.URI /* Displays the web view for the Amazon Q tool window */ -class Browser(parent: Disposable) : Disposable { + +class Browser(parent: Disposable, private val webUri: URI, val project: Project) : Disposable { val jcefBrowser = createBrowser(parent) @@ -39,7 +46,15 @@ class Browser(parent: Disposable) : Disposable { AssetResourceHandler.AssetResourceHandlerFactory(), ) - loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand, activeProfile) + loadWebView( + isCodeTransformAvailable, + isFeatureDevAvailable, + isDocAvailable, + isCodeScanAvailable, + isCodeTestAvailable, + highlightCommand, + activeProfile + ) } override fun dispose() { @@ -48,10 +63,14 @@ class Browser(parent: Disposable) : Disposable { fun component() = jcefBrowser.component - fun post(message: String) = + fun postChat(command: FlareUiMessage) = postChat(Gson().toJson(command)) + + @Deprecated("shouldn't need this version") + fun postChat(message: String) { jcefBrowser .cefBrowser - .executeJavaScript("window.postMessage(JSON.stringify($message))", jcefBrowser.cefBrowser.url, 0) + .executeJavaScript("window.postMessage($message)", jcefBrowser.cefBrowser.url, 0) + } // Load the chat web app into the jcefBrowser private fun loadWebView( @@ -94,33 +113,119 @@ class Browser(parent: Disposable) : Disposable { activeProfile: QRegionProfile?, ): String { val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)") - + val connectorAdapterPath = "http://mynah/js/connectorAdapter.js" + generateQuickActionConfig() + // https://github.com/highlightjs/highlight.js/issues/1387 + // language=HTML val jsScripts = """ - + + + """.trimIndent() + addQuickActionCommands( + isCodeTransformAvailable, + isFeatureDevAvailable, + isDocAvailable, + isCodeTestAvailable, + isCodeScanAvailable, + highlightCommand, + activeProfile + ) return """ + AWS Q @@ -132,8 +237,32 @@ class Browser(parent: Disposable) : Disposable { """.trimIndent() } + private fun addQuickActionCommands( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeTestAvailable: Boolean, + isCodeScanAvailable: Boolean, + highlightCommand: HighlightCommand?, + activeProfile: QRegionProfile?, + ) { + // TODO: Remove this once chat has been integrated with agents. This is added temporarily to keep detekt happy. + isCodeScanAvailable + isCodeTestAvailable + isDocAvailable + isFeatureDevAvailable + isCodeTransformAvailable + MAX_ONBOARDING_PAGE_COUNT + OBJECT_MAPPER + highlightCommand + activeProfile + } + + private fun generateQuickActionConfig() = AwsServerCapabilitiesProvider.getInstance(project).getChatOptions().quickActions.quickActionsCommandGroups + .let { OBJECT_MAPPER.writeValueAsString(it) } + ?: "[]" + companion object { - private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js" private const val MAX_ONBOARDING_PAGE_COUNT = 3 private val OBJECT_MAPPER = jacksonObjectMapper() } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 592c588550e..83d6a532ef1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -1,11 +1,20 @@ // Copyright 2023 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.webview +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.module.kotlin.treeToValue +import com.google.gson.Gson import com.intellij.ide.BrowserUtil import com.intellij.ide.util.RunOnceUtil +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.project.Project import com.intellij.ui.jcef.JBCefJSQuery.Response +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope @@ -17,22 +26,88 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.cef.browser.CefBrowser +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode +import software.aws.toolkits.core.utils.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageSerializer +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQChatServer +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcMethod +import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcNotification +import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcRequest +import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatAsyncResultManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AUTH_FOLLOW_UP_CLICKED +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AuthFollowUpClickNotification +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ButtonClickResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_BUTTON_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_CONVERSATION_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_COPY_CODE_TO_CLIPBOARD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_CREATE_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_DISCLAIMER_ACKNOWLEDGED +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FEEDBACK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FILE_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FOLLOW_UP_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_INFO_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_INSERT_TO_CURSOR +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_LIST_CONVERSATIONS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPEN_TAB +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_PROMPT_OPTION_ACKNOWLEDGED +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_QUICK_ACTION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_READY +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SOURCE_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_ADD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_BAR_ACTIONS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_CHANGE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResponse +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_SETTINGS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_WORKSPACE_SETTINGS_KEY +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenSettingsNotification +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenTabResponse +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenTabResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenTabResultError +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenTabResultSuccess +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.PROMPT_INPUT_OPTIONS_CHANGE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.QuickChatActionRequest +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.STOP_CHAT_RESPONSE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendChatPromptRequest +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.StopResponseMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TELEMETRY_EVENT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil.toUriString import software.aws.toolkits.jetbrains.services.amazonq.util.command import software.aws.toolkits.jetbrains.services.amazonq.util.tabType import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.AmazonQTheme import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter +import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable import software.aws.toolkits.jetbrains.settings.MeetQSettings import software.aws.toolkits.telemetry.MetricResult import software.aws.toolkits.telemetry.Telemetry +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException import java.util.function.Function class BrowserConnector( private val serializer: MessageSerializer = MessageSerializer.getInstance(), private val themeBrowserAdapter: ThemeBrowserAdapter = ThemeBrowserAdapter(), + private val project: Project, ) { var uiReady = CompletableDeferred() + private val chatCommunicationManager = ChatCommunicationManager.getInstance(project) + private val chatAsyncResultManager = ChatAsyncResultManager.getInstance(project) suspend fun connect( browser: Browser, @@ -43,14 +118,15 @@ class BrowserConnector( .onEach { json -> val node = serializer.toNode(json) when (node.command) { + // this is sent when the named agents UI is ready "ui-is-ready" -> { uiReady.complete(true) + chatCommunicationManager.setUiReady() RunOnceUtil.runOnceForApp("AmazonQ-UI-Ready") { MeetQSettings.getInstance().reinvent2024OnboardingCount += 1 } } - - "disclaimer-acknowledged" -> { + CHAT_DISCLAIMER_ACKNOWLEDGED -> { MeetQSettings.getInstance().disclaimerAcknowledged = true } @@ -77,11 +153,15 @@ class BrowserConnector( } } - val tabType = node.tabType ?: return@onEach - connections.filter { connection -> connection.app.tabTypes.contains(tabType) }.forEach { connection -> - launch { - val message = serializer.deserialize(node, connection.messageTypeRegistry) - connection.messagesFromUiToApp.publish(message) + val tabType = node.tabType + if (tabType == null || tabType == "cwc") { + handleFlareChatMessages(browser, node) + } else { + connections.filter { connection -> connection.app.tabTypes.contains(tabType) }.forEach { connection -> + launch { + val message = serializer.deserialize(node, connection.messageTypeRegistry) + connection.messagesFromUiToApp.publish(message) + } } } } @@ -90,22 +170,28 @@ class BrowserConnector( // Wait for UI ready before starting to send messages to the UI. uiReady.await() + // Chat options including history and quick actions need to be sent in once UI is ready + val showChatOptions = """{ + "command": "chatOptions", + "params": ${Gson().toJson(AwsServerCapabilitiesProvider.getInstance(project).getChatOptions())} + } + """.trimIndent() + browser.postChat(showChatOptions) + // Send inbound messages to the browser val inboundMessages = connections.map { it.messagesFromAppToUi.flow }.merge() inboundMessages - .onEach { browser.post(serializer.serialize(it)) } + .onEach { browser.postChat(serializer.serialize(it)) } .launchIn(this) } suspend fun connectTheme( chatBrowser: CefBrowser, - loginBrowser: CefBrowser, themeSource: Flow, ) = coroutineScope { themeSource .distinctUntilChanged() .onEach { - themeBrowserAdapter.updateLoginThemeInBrowser(loginBrowser, it) themeBrowserAdapter.updateThemeInBrowser(chatBrowser, it, uiReady) } .launchIn(this) @@ -123,4 +209,355 @@ class BrowserConnector( browser.receiveMessageQuery.removeHandler(handler) } } + + private fun handleFlareChatMessages(browser: Browser, node: JsonNode) { + when (node.command) { + SEND_CHAT_COMMAND_PROMPT -> { + val requestFromUi = serializer.deserializeChatMessages(node) + val editor = FileEditorManager.getInstance(project).selectedTextEditor + val textDocumentIdentifier = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) } + val cursorState = editor?.let { LspEditorUtil.getCursorState(it) } + + val enrichmentParams = mapOf( + "textDocument" to textDocumentIdentifier, + "cursorState" to cursorState, + ) + + val serializedEnrichmentParams = serializer.objectMapper.valueToTree(enrichmentParams) + val chatParams: ObjectNode = (node.params as ObjectNode) + .setAll(serializedEnrichmentParams) + + val tabId = requestFromUi.params.tabId + val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId) + chatCommunicationManager.registerPartialResultToken(partialResultToken) + + var encryptionManager: JwtEncryptionManager? = null + val result = AmazonQLspService.executeIfRunning(project) { server -> + encryptionManager = this.encryptionManager + + val encryptedParams = EncryptedChatParams(this.encryptionManager.encrypt(chatParams), partialResultToken) + rawEndpoint.request(SEND_CHAT_COMMAND_PROMPT, encryptedParams) as CompletableFuture + } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) + + // We assume there is only one outgoing request per tab because the input is + // blocked when there is an outgoing request + chatCommunicationManager.setInflightRequestForTab(tabId, result) + showResult(result, partialResultToken, tabId, encryptionManager, browser) + } + + CHAT_QUICK_ACTION -> { + val requestFromUi = serializer.deserializeChatMessages(node) + val tabId = requestFromUi.params.tabId + val quickActionParams = node.params ?: error("empty payload") + val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId) + chatCommunicationManager.registerPartialResultToken(partialResultToken) + var encryptionManager: JwtEncryptionManager? = null + val result = AmazonQLspService.executeIfRunning(project) { server -> + encryptionManager = this.encryptionManager + + val encryptedParams = EncryptedQuickActionChatParams(this.encryptionManager.encrypt(quickActionParams), partialResultToken) + rawEndpoint.request(CHAT_QUICK_ACTION, encryptedParams) as CompletableFuture + } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) + + // We assume there is only one outgoing request per tab because the input is + // blocked when there is an outgoing request + chatCommunicationManager.setInflightRequestForTab(tabId, result) + + showResult(result, partialResultToken, tabId, encryptionManager, browser) + } + + CHAT_LIST_CONVERSATIONS -> { + handleChat(AmazonQChatServer.listConversations, node) + .whenComplete { response, _ -> + browser.postChat( + FlareUiMessage( + command = CHAT_LIST_CONVERSATIONS, + params = response + ) + ) + } + } + + CHAT_CONVERSATION_CLICK -> { + handleChat(AmazonQChatServer.conversationClick, node) + .whenComplete { response, _ -> + browser.postChat( + FlareUiMessage( + command = CHAT_CONVERSATION_CLICK, + params = response + ) + ) + } + } + + CHAT_FEEDBACK -> { + handleChat(AmazonQChatServer.feedback, node) + } + + CHAT_READY -> { + handleChat(AmazonQChatServer.chatReady, node) { params, invoke -> + uiReady.complete(true) + chatCommunicationManager.setUiReady() + RunOnceUtil.runOnceForApp("AmazonQ-UI-Ready") { + MeetQSettings.getInstance().reinvent2024OnboardingCount += 1 + } + + invoke() + } + } + + CHAT_TAB_ADD -> { + handleChat(AmazonQChatServer.tabAdd, node) + } + + CHAT_TAB_REMOVE -> { + handleChat(AmazonQChatServer.tabRemove, node) { params, invoke -> + chatCommunicationManager.removePartialChatMessage(params.tabId) + cancelInflightRequests(params.tabId) + + invoke() + } + } + + CHAT_TAB_CHANGE -> { + handleChat(AmazonQChatServer.tabChange, node) + } + + CHAT_OPEN_TAB -> { + val response = serializer.deserializeChatMessages(node) + val future = chatCommunicationManager.removeTabOpenRequest(response.requestId) ?: return + try { + val id = serializer.deserializeChatMessages(node.params).result.tabId + future.complete(OpenTabResult(id)) + } catch (e: Exception) { + try { + val err = serializer.deserializeChatMessages(node.params) + future.complete(err.error) + } catch (_: Exception) { + future.completeExceptionally(e) + } + } + } + + CHAT_INSERT_TO_CURSOR -> { + handleChat(AmazonQChatServer.insertToCursorPosition, node) + } + + CHAT_LINK_CLICK -> { + handleChat(AmazonQChatServer.linkClick, node) + } + + CHAT_INFO_LINK_CLICK -> { + handleChat(AmazonQChatServer.infoLinkClick, node) + } + + CHAT_SOURCE_LINK_CLICK -> { + handleChat(AmazonQChatServer.sourceLinkClick, node) + } + + CHAT_FILE_CLICK -> { + handleChat(AmazonQChatServer.fileClick, node) + } + + PROMPT_INPUT_OPTIONS_CHANGE -> { + handleChat(AmazonQChatServer.promptInputOptionsChange, node) + } + + CHAT_FOLLOW_UP_CLICK -> { + handleChat(AmazonQChatServer.followUpClick, node) + } + + CHAT_BUTTON_CLICK -> { + handleChat(AmazonQChatServer.buttonClick, node).thenApply { response -> + if (response is ButtonClickResult && !response.success) { + LOG.warn { "Failed to execute action associated with button with reason: ${response.failureReason}" } + } + } + } + + CHAT_COPY_CODE_TO_CLIPBOARD -> { + handleChat(AmazonQChatServer.copyCodeToClipboard, node) + } + + GET_SERIALIZED_CHAT_REQUEST_METHOD -> { + val response = serializer.deserializeChatMessages(node) + chatCommunicationManager.completeSerializedChatResponse( + response.requestId, + response.params.result.content + ) + } + + CHAT_TAB_BAR_ACTIONS -> { + handleChat(AmazonQChatServer.tabBarActions, node) { params, invoke -> + invoke() + .whenComplete { actions, error -> + try { + if (error != null) { + throw error + } + + browser.postChat( + FlareUiMessage( + command = CHAT_TAB_BAR_ACTIONS, + params = actions + ) + ) + } catch (e: Exception) { + val cause = if (e is CompletionException) e.cause else e + + // dont post error to UI if user cancels export + if (cause is ResponseErrorException && cause.responseError.code == ResponseErrorCode.RequestCancelled.getValue()) { + return@whenComplete + } + LOG.error { "Failed to perform chat tab bar action $e" } + params.tabId?.let { + browser.postChat(chatCommunicationManager.getErrorUiMessage(it, e, null)) + } + } + } + } + } + + CHAT_CREATE_PROMPT -> { + handleChat(AmazonQChatServer.createPrompt, node) + } + + STOP_CHAT_RESPONSE -> { + val stopResponseRequest = serializer.deserializeChatMessages(node) + if (!chatCommunicationManager.hasInflightRequest(stopResponseRequest.params.tabId)) { + return + } + cancelInflightRequests(stopResponseRequest.params.tabId) + chatCommunicationManager.removePartialChatMessage(stopResponseRequest.params.tabId) + } + + AUTH_FOLLOW_UP_CLICKED -> { + val message = serializer.deserializeChatMessages(node) + chatCommunicationManager.handleAuthFollowUpClicked( + project, + message.params + ) + } + + CHAT_PROMPT_OPTION_ACKNOWLEDGED -> { + val acknowledgedMessage = node.params?.get("messageId") + if (acknowledgedMessage?.asText() == "programmerModeCardId") { + MeetQSettings.getInstance().pairProgrammingAcknowledged = true + } + } + + OPEN_SETTINGS -> { + val openSettingsNotification = serializer.deserializeChatMessages(node) + if (openSettingsNotification.params.settingKey != OPEN_WORKSPACE_SETTINGS_KEY) return + runInEdt { + ShowSettingsUtil.getInstance().showSettingsDialog(browser.project, CodeWhispererConfigurable::class.java) + } + } + TELEMETRY_EVENT -> { + handleChat(AmazonQChatServer.telemetryEvent, node) + } + } + } + + private fun showResult( + result: CompletableFuture, + partialResultToken: String, + tabId: String, + encryptionManager: JwtEncryptionManager?, + browser: Browser, + ) { + result.whenComplete { value, error -> + try { + if (error != null) { + throw error + } + chatCommunicationManager.removePartialChatMessage(partialResultToken) + val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat( + SEND_CHAT_COMMAND_PROMPT, + tabId, + value?.let { encryptionManager?.decrypt(it) }.orEmpty(), + isPartialResult = false + ) + browser.postChat(messageToChat) + chatCommunicationManager.removeInflightRequestForTab(tabId) + } catch (e: CancellationException) { + LOG.warn { "Cancelled chat generation" } + try { + chatAsyncResultManager.createRequestId(partialResultToken) + chatAsyncResultManager.getResult(partialResultToken) + handleCancellation(tabId, browser) + } catch (ex: Exception) { + LOG.warn(ex) { "An error occurred while processing cancellation" } + } finally { + chatAsyncResultManager.removeRequestId(partialResultToken) + chatCommunicationManager.removePartialResultLock(partialResultToken) + chatCommunicationManager.removeFinalResultProcessed(partialResultToken) + } + } catch (e: Exception) { + LOG.warn(e) { "Failed to send chat message" } + browser.postChat(chatCommunicationManager.getErrorUiMessage(tabId, e, partialResultToken)) + } + } + } + + private fun handleCancellation(tabId: String, browser: Browser) { + // Send a message to hide the stop button without showing an error + val cancelMessage = chatCommunicationManager.getCancellationUiMessage(tabId) + browser.postChat(cancelMessage) + } + + private fun cancelInflightRequests(tabId: String) { + chatCommunicationManager.getInflightRequestForTab(tabId)?.let { request -> + request.cancel(true) + chatCommunicationManager.removeInflightRequestForTab(tabId) + } + } + + private inline fun handleChat( + lspMethod: JsonRpcMethod, + node: JsonNode, + crossinline serverAction: (params: Request, invokeService: () -> CompletableFuture) -> CompletableFuture, + ): CompletableFuture { + val requestFromUi = if (node.params == null) { + Unit as Request + } else { + serializer.deserializeChatMessages(node.params, lspMethod.params) + } + + return AmazonQLspService.executeIfRunning(project) { _ -> + val invokeService = when (lspMethod) { + is JsonRpcNotification -> { + // notify is Unit + { CompletableFuture.completedFuture(rawEndpoint.notify(lspMethod.name, node.params?.let { serializer.objectMapper.treeToValue(it) })) } + } + + is JsonRpcRequest -> { + { + rawEndpoint.request(lspMethod.name, node.params?.let { serializer.objectMapper.treeToValue(it) }).thenApply { + serializer.objectMapper.readValue( + Gson().toJson(it), + lspMethod.response + ) + } + } + } + } as () -> CompletableFuture + serverAction(requestFromUi, invokeService) + } ?: CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")) + } + + private inline fun handleChat( + lspMethod: JsonRpcMethod, + node: JsonNode, + ): CompletableFuture = handleChat( + lspMethod, + node, + ) { _, invokeService -> invokeService() } + + private val JsonNode.params + get() = get("params") + + companion object { + private val LOG = getLogger() + } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt index 1c78cbc2cae..bbf449eac59 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt @@ -16,6 +16,8 @@ data class AmazonQTheme( val defaultText: Color, val inactiveText: Color, val linkText: Color, + val lightText: Color, + val emptyText: Color, val background: Color, val border: Color, @@ -31,14 +33,14 @@ data class AmazonQTheme( val buttonBackground: Color, val secondaryButtonForeground: Color, val secondaryButtonBackground: Color, + val inputBorderFocused: Color, + val inputBorderUnfocused: Color, val info: Color, val success: Color, val warning: Color, val error: Color, - val cardBackground: Color, - val editorFont: Font, val editorBackground: Color, val editorForeground: Color, @@ -49,5 +51,5 @@ data class AmazonQTheme( val editorKeyword: Color, val editorString: Color, val editorProperty: Color, - + val editorClassName: Color, ) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt index 7fa543bf1d8..793a75052fb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt @@ -13,10 +13,13 @@ enum class CssVariable( FontFamily("--mynah-font-family"), TextColorDefault("--mynah-color-text-default"), + TextColorAlt("--mynah-color-text-alternate"), TextColorStrong("--mynah-color-text-strong"), TextColorWeak("--mynah-color-text-weak"), + TextColorLight("--mynah-color-light"), TextColorLink("--mynah-color-text-link"), TextColorInput("--mynah-color-text-input"), + TextColorDisabled("--mynah-color-text-disabled"), Background("--mynah-color-bg"), BackgroundAlt("--mynah-color-bg-alt"), @@ -25,7 +28,9 @@ enum class CssVariable( ColorDeep("--mynah-color-deep"), ColorDeepReverse("--mynah-color-deep-reverse"), BorderDefault("--mynah-color-border-default"), - InputBackground("--mynah-color-input-bg"), + BorderFocused("--mynah-color-text-input-border-focused"), + BorderUnfocused("--mynah-color-text-input-border"), + InputBackground("--mynah-input-bg"), SyntaxBackground("--mynah-color-syntax-bg"), SyntaxVariable("--mynah-color-syntax-variable"), @@ -36,6 +41,9 @@ enum class CssVariable( SyntaxProperty("--mynah-color-syntax-property"), SyntaxComment("--mynah-color-syntax-comment"), SyntaxCode("--mynah-color-syntax-code"), + SyntaxKeyword("--mynah-color-syntax-keyword"), + SyntaxString("--mynah-color-syntax-string"), + SyntaxClassName("--mynah-color-syntax-class-name"), SyntaxCodeFontFamily("--mynah-syntax-code-font-family"), SyntaxCodeFontSize("--mynah-syntax-code-font-size"), @@ -50,10 +58,9 @@ enum class CssVariable( SecondaryButtonBackground("--mynah-color-alternate"), SecondaryButtonForeground("--mynah-color-alternate-reverse"), - CodeText("--mynah-color-code-text"), - MainBackground("--mynah-color-main"), MainForeground("--mynah-color-main-reverse"), CardBackground("--mynah-card-bg"), + CardBackgroundAlt("--mynah-card-bg-alternate"), } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt index 4f56b127994..a845e3c19f3 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt @@ -113,11 +113,9 @@ class EditorThemeAdapter { warning = themeColor("Component.warningFocusColor", default = 0xE2A53A), error = themeColor("ProgressBar.failedColor", default = 0xD64F4F, darkDefault = 0xE74848), - cardBackground = cardBackground, - editorFont = currentScheme.getFont(EditorFontType.PLAIN), - editorBackground = chatBackground, - editorForeground = text, + editorBackground = currentScheme.defaultBackground, + editorForeground = currentScheme.defaultForeground, editorVariable = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.LOCAL_VARIABLE), editorOperator = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.OPERATION_SIGN), editorFunction = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.FUNCTION_DECLARATION), @@ -125,6 +123,11 @@ class EditorThemeAdapter { editorKeyword = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.KEYWORD), editorString = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.STRING), editorProperty = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.INSTANCE_FIELD), + editorClassName = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.CLASS_NAME), + lightText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63), + emptyText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63), + inputBorderFocused = themeColor("ActionButton.focusedBorderColor", default = 0x4682FA, darkDefault = 0x3574f0), + inputBorderUnfocused = themeColor("TextField.borderColor", default = 0xEBECF0, darkDefault = 0x4E5157), ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt index 77528733556..d0f43958664 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt @@ -26,6 +26,7 @@ class ThemeBrowserAdapter { } private fun buildJsCodeToUpdateTheme(theme: AmazonQTheme) = buildString { + val (bg, altBg, inputBg) = determineInputAndBgColor(theme) appendDarkMode(theme.darkMode) append("{\n") @@ -35,18 +36,24 @@ class ThemeBrowserAdapter { append(CssVariable.FontFamily, theme.font.toCssFontFamily()) append(CssVariable.TextColorDefault, theme.defaultText) + append(CssVariable.TextColorAlt, theme.defaultText) append(CssVariable.TextColorStrong, theme.textFieldForeground) append(CssVariable.TextColorInput, theme.textFieldForeground) append(CssVariable.TextColorLink, theme.linkText) - append(CssVariable.TextColorWeak, theme.inactiveText) - - append(CssVariable.Background, theme.background) - append(CssVariable.BackgroundAlt, theme.background) - append(CssVariable.CardBackground, theme.cardBackground) + append(CssVariable.TextColorWeak, theme.emptyText) + append(CssVariable.TextColorLight, theme.emptyText) + append(CssVariable.TextColorDisabled, theme.inactiveText) + + append(CssVariable.Background, bg) + append(CssVariable.BackgroundAlt, altBg) + append(CssVariable.CardBackground, bg) + append(CssVariable.CardBackgroundAlt, altBg) append(CssVariable.BorderDefault, theme.border) + append(CssVariable.BorderFocused, theme.inputBorderFocused) + append(CssVariable.BorderUnfocused, theme.inputBorderUnfocused) append(CssVariable.TabActive, theme.activeTab) - append(CssVariable.InputBackground, theme.textFieldBackground) + append(CssVariable.InputBackground, inputBg) append(CssVariable.ButtonBackground, theme.buttonBackground) append(CssVariable.ButtonForeground, theme.buttonForeground) @@ -63,6 +70,7 @@ class ThemeBrowserAdapter { append(CssVariable.SyntaxCodeFontFamily, theme.editorFont.toCssFontFamily("monospace")) append(CssVariable.SyntaxCodeFontSize, theme.editorFont.toCssSize()) + append(CssVariable.SyntaxCode, theme.editorForeground) append(CssVariable.SyntaxBackground, theme.editorBackground) append(CssVariable.SyntaxVariable, theme.editorVariable) append(CssVariable.SyntaxOperator, theme.editorOperator) @@ -71,7 +79,9 @@ class ThemeBrowserAdapter { append(CssVariable.SyntaxAttributeValue, theme.editorKeyword) append(CssVariable.SyntaxAttribute, theme.editorString) append(CssVariable.SyntaxProperty, theme.editorProperty) - append(CssVariable.SyntaxCode, theme.editorForeground) + append(CssVariable.SyntaxKeyword, theme.editorKeyword) + append(CssVariable.SyntaxString, theme.editorString) + append(CssVariable.SyntaxClassName, theme.editorClassName) append(CssVariable.MainBackground, theme.buttonBackground) append(CssVariable.MainForeground, theme.buttonForeground) @@ -104,4 +114,15 @@ class ThemeBrowserAdapter { // Some font names have characters that require them to be wrapped in quotes in the CSS variable, for example if they have spaces or a period. private fun Font.toCssFontFamily(fallback: String = "system-ui") = "\"$family\", $fallback" + + // darkest = bg, second darkest is alt bg, lightest is input bg + private fun determineInputAndBgColor(theme: AmazonQTheme): Triple { + val colors = arrayOf(theme.editorBackground, theme.background, theme.textFieldBackground).sortedWith( + Comparator.comparing { + // luma calculation for brightness + (0.2126 * it.red) + (0.7152 * it.green) + (0.0722 * it.blue) + } + ) + return Triple(colors[0], colors[1], colors[2]) + } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt index f135cfa18b4..920bebb5109 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt @@ -11,8 +11,6 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeSca import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.UpdatePlaceholderMessage import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import java.util.UUID @@ -36,7 +34,6 @@ class CodeScanChatHelper( clearPreviousItemButtons: Boolean? = false, ) { if (isInValidSession()) return - broadcastQEvent(QFeatureEvent.INVOCATION) messagePublisher.publish( CodeScanChatMessage( tabId = activeCodeScanTabId as String, 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 a03a5b3176e..71e495e7c6d 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 @@ -54,6 +54,9 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT 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.lsp.model.aws.textDocument.InlineCompletionReference +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReferencePosition +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage 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 @@ -74,8 +77,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendA import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.codewhisperer.util.isWithin import software.aws.toolkits.jetbrains.services.cwc.ChatConstants @@ -110,6 +111,12 @@ import java.util.UUID import software.amazon.awssdk.services.codewhispererstreaming.model.Position as StreamingPosition import software.amazon.awssdk.services.codewhispererstreaming.model.Range as StreamingRange +data class TestCommandMessage( + val sender: String = "codetest", + val command: String = "test", + val type: String = "addAnswer", +) : AmazonQMessage + class CodeTestChatController( private val context: AmazonQAppInitContext, private val chatSessionStorage: ChatSessionStorage, @@ -637,7 +644,21 @@ class CodeTestChatController( } LOG.debug { "Original code content from reference span: $originalContent" } withContext(EDT) { - manager.addReferenceLogPanelEntry(reference = reference, null, null, originalContent?.split("\n")) + // TODO flare: hook /test references with flare correctly, this is only a compile error fix which is not tested + manager.addReferenceLogPanelEntry( + reference = InlineCompletionReference( + referenceName = reference.repository(), + referenceUrl = reference.url(), + licenseName = reference.licenseName(), + position = InlineCompletionReferencePosition( + startCharacter = reference.recommendationContentSpan().start(), + endCharacter = reference.recommendationContentSpan().end() + ) + ), + null, + null, + originalContent?.split("\n") + ) manager.toolWindow?.show() } } @@ -1298,7 +1319,6 @@ class CodeTestChatController( "Processing message: $message " + "tabId: $tabId" } - broadcastQEvent(QFeatureEvent.INVOCATION) when (session.conversationState) { ConversationState.WAITING_FOR_BUILD_COMMAND_INPUT -> handleBuildCommandInput(session, message) ConversationState.WAITING_FOR_REGENERATE_INPUT -> handleRegenerateInput(session, message) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt index 9984b7142a0..94b840aae12 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt @@ -79,8 +79,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Delete import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.resources.message import java.util.UUID @@ -768,7 +766,6 @@ class DocController( else -> emptyList() } sendDocGenerationTelemetry(filePaths, session, docGenerationTask) - broadcastQEvent(QFeatureEvent.INVOCATION) if (filePaths.isNotEmpty()) { processOpenDiff( 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 4870a206809..8e1627b67d5 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 @@ -75,8 +75,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.content import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl @@ -219,7 +217,6 @@ class FeatureDevController( logger.debug { "$FEATURE_NAME: Processing InsertCodeAtCursorPosition: $message" } withContext(EDT) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext val caret: Caret = editor.caretModel.primaryCaret @@ -231,7 +228,6 @@ class FeatureDevController( } editor.document.insertString(offset, message.code) } - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } } @@ -751,7 +747,6 @@ class FeatureDevController( } session.preloader(messenger) - broadcastQEvent(QFeatureEvent.INVOCATION) when (session.sessionState.phase) { SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt index 71110cba420..e5e92f7544b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt @@ -1,12 +1,25 @@ // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - +@file:Suppress("BannedImports") package software.aws.toolkits.jetbrains.services.cwc.commands +import com.google.gson.Gson +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.runBlocking +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GENERIC_COMMAND +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GenericCommandParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_TO_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller.TestCommandMessage +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType // Register Editor Actions in the Editor Context Menu class ActionRegistrar { @@ -15,11 +28,27 @@ class ActionRegistrar { val flow = _messages.asSharedFlow() fun reportMessageClick(command: EditorContextCommand, project: Project) { - _messages.tryEmit(ContextMenuActionMessage(command, project)) - } - - fun reportMessageClick(command: EditorContextCommand, issue: MutableMap, project: Project) { - _messages.tryEmit(CodeScanIssueActionMessage(command, issue, project)) + if (command == EditorContextCommand.GenerateUnitTests) { + AsyncChatUiListener.notifyPartialMessageUpdate(project, Gson().toJson(TestCommandMessage())) + } else { + // new agentic chat route + ApplicationManager.getApplication().executeOnPooledThread { + runBlocking { + val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) + val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ContextMenu) + val codeSelection = "\n```\n${fileContext.focusAreaContext?.codeSelection?.trimIndent()?.trim()}\n```\n" + var uiMessage: FlareUiMessage? = null + if (command.verb != SEND_TO_PROMPT) { + val params = GenericCommandParams(selection = codeSelection, triggerType = TriggerType.CONTEXT_MENU, genericCommand = command.name) + uiMessage = FlareUiMessage(command = GENERIC_COMMAND, params = params) + } else { + val params = SendToPromptParams(selection = codeSelection, triggerType = TriggerType.CONTEXT_MENU) + uiMessage = FlareUiMessage(command = SEND_TO_PROMPT, params = params) + } + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) + } + } + } } // provide singleton access diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt index d7b8f8d0a42..33a9cc50baa 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt @@ -31,7 +31,7 @@ enum class EditorContextCommand( actionId = "aws.amazonq.generateUnitTests", ), SendToPrompt( - verb = "SendToPrompt", + verb = "sendToPrompt", actionId = "aws.amazonq.sendToPrompt", ), ExplainCodeScanIssue( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanQActions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanQActions.kt deleted file mode 100644 index 1dba4220223..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanQActions.kt +++ /dev/null @@ -1,25 +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.cwc.commands.codescan.actions - -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataKey -import com.intellij.openapi.project.DumbAware -import software.aws.toolkits.jetbrains.services.cwc.commands.ActionRegistrar -import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand - -open class CodeScanQActions(private val command: EditorContextCommand) : AnAction(), DumbAware { - override fun actionPerformed(e: AnActionEvent) { - val issueDataKey = DataKey.create>("amazonq.codescan.explainissue") - val issueContext = e.getData(issueDataKey) ?: return - val project = e.getData(CommonDataKeys.PROJECT) ?: return - - ActionManager.getInstance().getAction("q.openchat").actionPerformed(e) - - ActionRegistrar.instance.reportMessageClick(command, issueContext, project) - } -} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt index 48a10d2a381..94f572984a9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt @@ -3,6 +3,63 @@ package software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions -import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.DumbAware +import kotlinx.coroutines.runBlocking +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_TO_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType -class ExplainCodeIssueAction : CodeScanQActions(EditorContextCommand.ExplainCodeScanIssue) +class ExplainCodeIssueAction : AnAction(), DumbAware { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = e.project != null + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val issueDataKey = DataKey.create>("amazonq.codescan.explainissue") + val issueContext = e.getData(issueDataKey) ?: return + + ActionManager.getInstance().getAction("q.openchat").actionPerformed(e) + + ApplicationManager.getApplication().executeOnPooledThread { + runBlocking { + // https://github.com/aws/aws-toolkit-vscode/blob/master/packages/amazonq/src/lsp/chat/commands.ts#L30 + val codeSelection = "\n```\n${issueContext["code"]?.trimIndent()?.trim()}\n```\n" + + val prompt = "Explain the issue \n\n " + + "Issue: \"${issueContext["title"]}\" \n" + + "Code: $codeSelection" + + val modelPrompt = "Explain the issue ${issueContext["title"]} \n\n " + + "Issue: \"${issueContext["title"]}\" \n" + + "Description: ${issueContext["description"]} \n" + + "Code: $codeSelection and generate the code demonstrating the fix" + + val params = SendToPromptParams( + selection = codeSelection, + triggerType = TriggerType.CONTEXT_MENU, + prompt = ChatPrompt( + prompt = prompt, + escapedPrompt = modelPrompt, + command = null + ), + autoSubmit = true + ) + + val uiMessage = FlareUiMessage(SEND_TO_PROMPT, params) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) + } + } + } +} 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 dc604b1393a..a47329ed567 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 @@ -19,9 +19,7 @@ import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.psi.PsiDocumentManager import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -37,22 +35,14 @@ 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.coroutines.EDT -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.auth.AuthNeededState -import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher 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.project.ProjectContextController import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticDifferences -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics import software.aws.toolkits.jetbrains.services.cwc.InboundAppMessagesHandler import software.aws.toolkits.jetbrains.services.cwc.clients.chat.exceptions.ChatApiException import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData @@ -61,11 +51,9 @@ import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerTy import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionFactoryV1 import software.aws.toolkits.jetbrains.services.cwc.commands.CodeScanIssueActionMessage import software.aws.toolkits.jetbrains.services.cwc.commands.ContextMenuActionMessage -import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticPrompt import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticTextResponse import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.InsertedCodeModificationEntry import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer @@ -81,20 +69,11 @@ import software.aws.toolkits.jetbrains.services.cwc.messages.FocusType import software.aws.toolkits.jetbrains.services.cwc.messages.FollowUp import software.aws.toolkits.jetbrains.services.cwc.messages.IncomingCwcMessage import software.aws.toolkits.jetbrains.services.cwc.messages.OnboardingPageInteractionMessage -import software.aws.toolkits.jetbrains.services.cwc.messages.OpenSettingsMessage import software.aws.toolkits.jetbrains.services.cwc.messages.QuickActionMessage import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.telemetry.CwsprChatCommandType -import java.time.Instant import java.util.UUID -data class TestCommandMessage( - val sender: String = "codetest", - val command: String = "test", - val type: String = "addAnswer", -) : AmazonQMessage - class ChatController private constructor( private val context: AmazonQAppInitContext, private val chatSessionStorage: ChatSessionStorage, @@ -137,24 +116,11 @@ class ChatController private constructor( } override suspend fun processPromptChatMessage(message: IncomingCwcMessage.ChatPrompt) { - var prompt = message.chatMessage - var queryResult: List = emptyList() + val prompt = message.chatMessage + val queryResult: List = emptyList() val triggerId = UUID.randomUUID().toString() - var shouldAddIndexInProgressMessage: Boolean = false - var shouldUseWorkspaceContext: Boolean = false - - if (prompt.contains("@workspace")) { - if (CodeWhispererSettings.getInstance().isProjectContextEnabled()) { - shouldUseWorkspaceContext = true - prompt = prompt.replace("@workspace", "") - val projectContextController = ProjectContextController.getInstance(context.project) - queryResult = projectContextController.queryChat(prompt, timeout = null) - if (!projectContextController.getProjectContextIndexComplete()) shouldAddIndexInProgressMessage = true - logger.info { "project context relevant document count: ${queryResult.size}" } - } else { - sendOpenSettingsMessage(message.tabId) - } - } + val shouldAddIndexInProgressMessage = false + val shouldUseWorkspaceContext = false handleChat( tabId = message.tabId, @@ -211,14 +177,12 @@ class ChatController private constructor( } override suspend fun processInsertCodeAtCursorPosition(message: IncomingCwcMessage.InsertCodeAtCursorPosition) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) withContext(EDT) { val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext val caret: Caret = editor.caretModel.primaryCaret val offset: Int = caret.offset - val oldDiagnostics = getDocumentDiagnostics(editor.document, context.project) ApplicationManager.getApplication().runWriteAction { WriteCommandAction.runWriteCommandAction(context.project) { if (caret.hasSelection()) { @@ -228,29 +192,10 @@ class ChatController private constructor( editor.document.insertString(offset, message.code) ReferenceLogController.addReferenceLog(message.code, message.codeReference, editor, context.project, null) - - CodeWhispererUserModificationTracker.getInstance(context.project).enqueue( - InsertedCodeModificationEntry( - telemetryHelper.getConversationId(message.tabId).orEmpty(), - message.messageId, - Instant.now(), - PsiDocumentManager.getInstance(context.project).getPsiFile(editor.document)?.virtualFile, - editor.document.createRangeMarker(caret.selectionStart, caret.selectionEnd, true), - message.code, - ), - ) } } - if (isInternalUser(getStartUrl(context.project))) { - // wait for the IDE itself to update its diagnostics for current file - delay(500) - val newDiagnostics = getDocumentDiagnostics(editor.document, context.project) - message.diagnosticsDifferences = getDiagnosticDifferences(oldDiagnostics, newDiagnostics) - } } telemetryHelper.recordInteractWithMessage(message) - - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } override suspend fun processStopResponseMessage(message: IncomingCwcMessage.StopResponse) { @@ -332,33 +277,7 @@ class ChatController private constructor( // JB specific (not in vscode) override suspend fun processContextMenuCommand(message: ContextMenuActionMessage) { - // Extract context - if (message.project != context.project) { - return - } - val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ContextMenu) - val triggerId = UUID.randomUUID().toString() - val codeSelection = "\n```\n${fileContext.focusAreaContext?.codeSelection?.trimIndent()?.trim()}\n```\n" - - if (message.command == EditorContextCommand.SendToPrompt) { - messagePublisher.publish( - EditorContextCommandMessage( - message = codeSelection, - command = message.command.actionId, - triggerId = triggerId, - ), - ) - return - } - if (message.command == EditorContextCommand.GenerateUnitTests) { - // Publish an event to "codetest" tab with command as "test" and type as "addAnswer" - val messageToPublish = TestCommandMessage() - context.messagesFromAppToUi.publish(messageToPublish) - } else { - // Create prompt - val prompt = "${message.command} the following part of my code for me: $codeSelection" - processPromptActions(prompt, message, triggerId, fileContext) - } + // No-op since context commands are handled elsewhere. This function will be deprecated once we remove this class } private suspend fun processPromptActions( @@ -444,7 +363,6 @@ class ChatController private constructor( sessionInfo.history.add(requestData) telemetryHelper.recordEnterFocusConversation(tabId) telemetryHelper.recordStartConversation(tabId, requestData) - broadcastQEvent(QFeatureEvent.INVOCATION) // Send the request to the API and publish the responses back to the UI. // This is launched in a scope attached to the sessionInfo so that the Job can be cancelled on a per-session basis. ChatPromptHandler(telemetryHelper).handle(tabId, triggerId, requestData, sessionInfo, shouldAddIndexInProgressMessage) @@ -499,13 +417,6 @@ class ChatController private constructor( messagePublisher.publish(message) } - private suspend fun sendOpenSettingsMessage(tabId: String) { - val message = OpenSettingsMessage( - tabId = tabId - ) - messagePublisher.publish(message) - } - private suspend fun sendStaticTextResponse(tabId: String, triggerId: String, response: StaticTextResponse) { val chatMessage = ChatMessage( tabId = tabId, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt index 13537f5b933..b46660d0793 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt @@ -5,8 +5,8 @@ package software.aws.toolkits.jetbrains.services.cwc.controller import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import software.amazon.awssdk.services.codewhispererruntime.model.Reference -import software.amazon.awssdk.services.codewhispererruntime.model.Span +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReferencePosition import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition @@ -15,21 +15,18 @@ import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference object ReferenceLogController { fun addReferenceLog(originalCode: String, codeReferences: List?, editor: Editor, project: Project, inlineChatStartPosition: CaretPosition?) { + // TODO flare: hook /dev references with flare correctly, this is only a compile error fix which is not tested codeReferences?.let { references -> val cwReferences = references.map { reference -> - Reference.builder() - .licenseName(reference.licenseName) - .repository(reference.repository) - .url(reference.url) - .recommendationContentSpan( - reference.recommendationContentSpan?.let { span -> - Span.builder() - .start(span.start) - .end(span.end) - .build() - } + InlineCompletionReference( + referenceName = reference.repository.orEmpty(), + referenceUrl = reference.url.orEmpty(), + licenseName = reference.licenseName.orEmpty(), + position = InlineCompletionReferencePosition( + startCharacter = reference.recommendationContentSpan?.start ?: 0, + endCharacter = reference.recommendationContentSpan?.end ?: 0, ) - .build() + ) } val manager = CodeWhispererCodeReferenceManager.getInstance(project) @@ -38,7 +35,6 @@ object ReferenceLogController { cwReferences, editor, inlineChatStartPosition ?: CodeWhispererEditorUtil.getCaretPosition(editor), - null, ) } } @@ -46,21 +42,17 @@ object ReferenceLogController { fun addReferenceLog(codeReferences: List?, project: Project) { val manager = CodeWhispererCodeReferenceManager.getInstance(project) + // TODO flare: hook /dev references with flare correctly, this is only a compile error fix which is not tested codeReferences?.forEach { reference -> - val cwReferences = Reference.builder() - .licenseName(reference.licenseName) - .repository(reference.repository) - .url(reference.url) - .recommendationContentSpan( - reference.recommendationContentSpan?.let { span -> - Span.builder() - .start(span.start) - .end(span.end) - .build() - } + val cwReferences = InlineCompletionReference( + referenceName = reference.repository.orEmpty(), + referenceUrl = reference.url.orEmpty(), + licenseName = reference.licenseName.orEmpty(), + position = InlineCompletionReferencePosition( + startCharacter = reference.recommendationContentSpan?.start ?: 0, + endCharacter = reference.recommendationContentSpan?.end ?: 0, ) - .build() - + ) manager.addReferenceLogPanelEntry(reference = cwReferences, null, null, null) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index 72c2f4a0355..0b6c01b805b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -13,8 +13,6 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.codewhispererstreaming.model.CodeWhispererStreamingException import software.aws.toolkits.core.utils.convertMarkdownToHTML import software.aws.toolkits.core.utils.extractCodeBlockLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.clients.chat.exceptions.ChatApiException import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatResponseEvent @@ -117,8 +115,6 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { ) telemetryHelper.recordAddMessage(data, response, responseText.length, statusCode, countTotalNumberOfCodeBlocks(responseText)) emit(response) - - broadcastQEvent(QFeatureEvent.INVOCATION) } .catch { exception -> val statusCode = if (exception is AwsServiceException) exception.statusCode() else 0 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 a59cd3a4734..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 @@ -270,8 +270,6 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: acceptedCharacterCount(message.code.length) acceptedLineCount(message.code.lines().size) hasProjectLevelContext(getMessageHasProjectContext(message.messageId)) - addedIdeDiagnostics(message.diagnosticsDifferences?.added) - removedIdeDiagnostics(message.diagnosticsDifferences?.removed) }.build() } 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 bf468976a2f..1ea02ab6632 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 @@ -60,8 +60,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSe 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 -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController @@ -545,7 +543,6 @@ class InlineChatController( private fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { lateinit var rangeMarker: RangeMarker - broadcastQEvent(QFeatureEvent.STARTS_EDITING) ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { @@ -555,12 +552,10 @@ class InlineChatController( highlightCodeWithBackgroundColor(editor, rangeMarker.startOffset, rangeMarker.endOffset, true) } } - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) return rangeMarker } private fun replaceString(document: Document, start: Int, end: Int, text: String) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { @@ -568,7 +563,6 @@ class InlineChatController( } } } - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } private fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean) { @@ -725,8 +719,6 @@ class InlineChatController( canPopupAbort.set(true) undoChanges() } - - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) return errorMessage } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index f909115084f..a257565f8d4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager -import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons @@ -18,7 +17,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.BorderLayout import java.awt.Dimension -import java.awt.Font import javax.swing.BorderFactory import javax.swing.JButton import javax.swing.JLabel @@ -95,12 +93,10 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() override fun getPreferredSize(): Dimension = Dimension(popupWidth, popupHeight) private fun createTextField(): JTextField = JTextField().apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(popupInputWidth, popupInputHeight) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private fun createButton(text: String): JButton = JButton(text).apply { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt index 13ce851dda5..f695ba49930 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt @@ -18,7 +18,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand -import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.FollowUpType import java.time.Instant @@ -96,7 +95,6 @@ sealed interface IncomingCwcMessage : CwcMessage { val codeBlockIndex: Int?, val totalCodeBlocks: Int?, val codeBlockLanguage: String?, - var diagnosticsDifferences: DiagnosticDifferences?, ) : IncomingCwcMessage, TabId, MessageId data class TriggerTabIdReceived( diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt index 3542140d033..267d159e906 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt @@ -44,7 +44,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitConte 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.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences import software.aws.toolkits.jetbrains.services.cwc.clients.chat.ChatSession import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.CodeNamesImpl @@ -476,7 +475,6 @@ class TelemetryHelperTest { val inserTionTargetType = "insertionTargetType" val eventId = "eventId" val code = "println()" - val diagnosticDifferences = DiagnosticDifferences(emptyList(), emptyList()) sut.recordInteractWithMessage( IncomingCwcMessage.InsertCodeAtCursorPosition( @@ -489,8 +487,7 @@ class TelemetryHelperTest { eventId, codeBlockIndex, totalCodeBlocks, - lang, - diagnosticDifferences + lang ) ) @@ -506,8 +503,6 @@ class TelemetryHelperTest { acceptedLineCount(code.lines().size) customizationArn(customizationArn) hasProjectLevelContext(false) - addedIdeDiagnostics(emptyList()) - removedIdeDiagnostics(emptyList()) }.build() ) ) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/EncoderServerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/EncoderServerTest.kt deleted file mode 100644 index 7fd76f2c044..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/EncoderServerTest.kt +++ /dev/null @@ -1,43 +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.amazonq.workspace.context - -import com.intellij.util.io.DigestUtil -import org.apache.commons.codec.digest.DigestUtils -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer -import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule -import java.math.BigInteger - -class EncoderServerTest { - @Rule @JvmField - val projectRule: CodeInsightTestFixtureRule = JavaCodeInsightTestFixtureRule() - private lateinit var encoderServer: EncoderServer - private val inputBytes = BigInteger(32, DigestUtil.random).toByteArray() - - @Before - fun setup() { - encoderServer = EncoderServer(projectRule.project) - } - - @Test - fun `test download artifacts validate hash if it does not match`() { - val wrongHash = "sha384:ad527e9583d3dc4be3d302bac17f8d5a64eb8f5ab536717982620232e4e4bad82d1041fb73ae27899e9e802f07f61567" - - val actual = encoderServer.validateHash(wrongHash, inputBytes) - assertThat(actual).isEqualTo(false) - } - - @Test - fun `test download artifacts validate hash if it matches`() { - val rightHash = "sha384:${DigestUtils.sha384Hex(inputBytes)}" - - val actual = encoderServer.validateHash(rightHash, inputBytes) - assertThat(actual).isEqualTo(true) - } -} diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextControllerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextControllerTest.kt deleted file mode 100644 index 385d269bee9..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextControllerTest.kt +++ /dev/null @@ -1,79 +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.amazonq.workspace.context - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.testFramework.ProjectExtension -import com.intellij.testFramework.junit5.TestDisposable -import com.intellij.testFramework.replaceService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension -import org.mockito.Mockito.mockConstruction -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext -import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer -import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings - -class ProjectContextControllerTest { - lateinit var sut: ProjectContextController - - val project: Project - get() = projectExtension.project - - private companion object { - @JvmField - @RegisterExtension - val projectExtension = ProjectExtension() - } - - @Test - fun `should start encoderServer if chat project context is disabled`(@TestDisposable disposable: Disposable) = runTest { - ApplicationManager.getApplication() - .replaceService( - CodeWhispererSettings::class.java, - mock { on { isProjectContextEnabled() } doReturn false }, - disposable - ) - - assertEncoderServerStarted() - } - - @Test - fun `should start encoderServer if chat project context is enabled`(@TestDisposable disposable: Disposable) = runTest { - ApplicationManager.getApplication() - .replaceService( - CodeWhispererSettings::class.java, - mock { on { isProjectContextEnabled() } doReturn true }, - disposable - ) - - assertEncoderServerStarted() - } - - private fun assertEncoderServerStarted() = runTest { - mockConstruction(EncoderServer::class.java).use { - // TODO: figure out how to make this testScope work -// val cs = TestScope(context = StandardTestDispatcher()) // not works and the test never finish - val cs = CoroutineScope(getCoroutineBgContext()) // works - - assertThat(it.constructed()).isEmpty() - sut = ProjectContextController(project, cs) - assertThat(it.constructed()).hasSize(1) - -// cs.advanceUntilIdle() - sut.initJob.join() - val encoderServer = it.constructed().first() - verify(encoderServer, times(1)).downloadArtifactsAndStartServer() - } - } -} diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextProviderTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextProviderTest.kt deleted file mode 100644 index 399bd114b57..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextProviderTest.kt +++ /dev/null @@ -1,524 +0,0 @@ -// Copyright 2024 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.workspace.context - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.github.tomakehurst.wiremock.client.WireMock.aResponse -import com.github.tomakehurst.wiremock.client.WireMock.any -import com.github.tomakehurst.wiremock.client.WireMock.equalTo -import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor -import com.github.tomakehurst.wiremock.client.WireMock.stubFor -import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo -import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig -import com.github.tomakehurst.wiremock.http.Body -import com.github.tomakehurst.wiremock.junit.WireMockRule -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.replaceService -import io.mockk.every -import io.mockk.spyk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.jupiter.api.assertThrows -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.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext -import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer -import software.aws.toolkits.jetbrains.services.amazonq.project.IndexRequest -import software.aws.toolkits.jetbrains.services.amazonq.project.IndexUpdateMode -import software.aws.toolkits.jetbrains.services.amazonq.project.InlineBm25Chunk -import software.aws.toolkits.jetbrains.services.amazonq.project.InlineContextTarget -import software.aws.toolkits.jetbrains.services.amazonq.project.LspMessage -import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider -import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider.FileCollectionResult -import software.aws.toolkits.jetbrains.services.amazonq.project.QueryChatRequest -import software.aws.toolkits.jetbrains.services.amazonq.project.QueryInlineCompletionRequest -import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument -import software.aws.toolkits.jetbrains.services.amazonq.project.UpdateIndexRequest -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings -import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule -import java.net.ConnectException -import kotlin.test.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class ProjectContextProviderTest { - @Rule - @JvmField - val projectRule: CodeInsightTestFixtureRule = JavaCodeInsightTestFixtureRule() - - @Rule - @JvmField - val disposableRule: DisposableRule = DisposableRule() - - @Rule - @JvmField - val wireMock: WireMockRule = createMockServer() - - private val project: Project - get() = projectRule.project - - private lateinit var encoderServer: EncoderServer - private lateinit var sut: ProjectContextProvider - - private val mapper = jacksonObjectMapper() - - private val dispatcher = StandardTestDispatcher() - - @Before - fun setup() { - encoderServer = spy(EncoderServer(project)) - encoderServer.stub { on { port } doReturn wireMock.port() } - encoderServer.stub { on { isNodeProcessRunning() } doReturn true } - sut = spyk(ProjectContextProvider(project, encoderServer, TestScope(context = dispatcher))) - - // initialization - stubFor(any(urlPathEqualTo("/initialize")).willReturn(aResponse().withStatus(200).withResponseBody(Body("initialize response")))) - - // build index - stubFor(any(urlPathEqualTo("/buildIndex")).willReturn(aResponse().withStatus(200).withResponseBody(Body("initialize response")))) - - // update index - stubFor(any(urlPathEqualTo("/updateIndexV2")).willReturn(aResponse().withStatus(200).withResponseBody(Body("initialize response")))) - - // query - stubFor( - any(urlPathEqualTo("/query")).willReturn( - aResponse() - .withStatus(200) - .withResponseBody(Body(validQueryChatResponse)) - ) - ) - stubFor( - any(urlPathEqualTo("/queryInlineProjectContext")).willReturn( - aResponse() - .withStatus(200) - .withResponseBody( - Body(validQueryInlineResponse) - ) - ) - ) - - stubFor( - any(urlPathEqualTo("/getUsage")) - .willReturn( - aResponse() - .withStatus(200) - .withResponseBody(Body(validGetUsageResponse)) - ) - ) - } - - @Test - fun `Lsp endpoint correctness`() { - assertThat(LspMessage.Initialize.endpoint).isEqualTo("initialize") - assertThat(LspMessage.Index.endpoint).isEqualTo("buildIndex") - assertThat(LspMessage.UpdateIndex.endpoint).isEqualTo("updateIndexV2") - assertThat(LspMessage.QueryChat.endpoint).isEqualTo("query") - assertThat(LspMessage.QueryInlineCompletion.endpoint).isEqualTo("queryInlineProjectContext") - assertThat(LspMessage.GetUsageMetrics.endpoint).isEqualTo("getUsage") - } - - @Test - fun `index should send files within the project to lsp - vector index enabled`() = runTest { - ApplicationManager.getApplication().replaceService( - CodeWhispererSettings::class.java, - mock { on { isProjectContextEnabled() } doReturn true }, - disposableRule.disposable - ) - - projectRule.fixture.addFileToProject("Foo.java", "foo") - projectRule.fixture.addFileToProject("Bar.java", "bar") - projectRule.fixture.addFileToProject("Baz.java", "baz") - every { sut.collectFiles() } returns FileCollectionResult( - files = listOf("Foo.java", "Bar.java", "Baz.java"), - fileSize = 10 - ) - sut.index() - - val request = IndexRequest(listOf("/src/Foo.java", "/src/Bar.java", "/src/Baz.java"), "/src", "all", "") - assertThat(request.filePaths).hasSize(3) - assertThat(request.filePaths).satisfies({ - it.contains("/src/Foo.java") && - it.contains("/src/Baz.java") && - it.contains("/src/Bar.java") - }) - assertThat(request.config).isEqualTo("all") - - wireMock.verify( - 1, - postRequestedFor(urlPathEqualTo("/buildIndex")) - .withHeader("Content-Type", equalTo("text/plain")) - // comment it out because order matters and will cause json string different -// .withRequestBody(equalTo(encryptedRequest)) - ) - } - - @Test - fun `index should send files within the project to lsp - vector index disabled`() = runTest { - ApplicationManager.getApplication().replaceService( - CodeWhispererSettings::class.java, - mock { on { isProjectContextEnabled() } doReturn false }, - disposableRule.disposable - ) - - projectRule.fixture.addFileToProject("Foo.java", "foo") - projectRule.fixture.addFileToProject("Bar.java", "bar") - projectRule.fixture.addFileToProject("Baz.java", "baz") - every { sut.collectFiles() } returns FileCollectionResult( - files = listOf("Foo.java", "Bar.java", "Baz.java"), - fileSize = 10 - ) - sut.index() - - val request = IndexRequest(listOf("/src/Foo.java", "/src/Bar.java", "/src/Baz.java"), "/src", "default", "") - assertThat(request.filePaths).hasSize(3) - assertThat(request.filePaths).satisfies({ - it.contains("/src/Foo.java") && - it.contains("/src/Baz.java") && - it.contains("/src/Bar.java") - }) - assertThat(request.config).isEqualTo("default") - - wireMock.verify( - 1, - postRequestedFor(urlPathEqualTo("/buildIndex")) - .withHeader("Content-Type", equalTo("text/plain")) - // comment it out because order matters and will cause json string different -// .withRequestBody(equalTo(encryptedRequest)) - ) - } - - @Test - fun `updateIndex should send correct encrypted request to lsp`() { - sut.updateIndex(listOf("foo.java"), IndexUpdateMode.UPDATE) - val request = UpdateIndexRequest(listOf("foo.java"), IndexUpdateMode.UPDATE.command) - val requestJson = mapper.writeValueAsString(request) - - assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree("""{ "filePaths": ["foo.java"], "mode": "update" }""")) - - val encryptedRequest = encoderServer.encrypt(requestJson) - - wireMock.verify( - 1, - postRequestedFor(urlPathEqualTo("/updateIndexV2")) - .withHeader("Content-Type", equalTo("text/plain")) - .withRequestBody(equalTo(encryptedRequest)) - ) - } - - @Test - fun `query should send correct encrypted request to lsp`() = runTest { - // use real time - withContext(Dispatchers.Default.limitedParallelism(1)) { - sut = ProjectContextProvider(project, encoderServer, this) - val r = sut.query("foo", null) - advanceUntilIdle() - - val request = QueryChatRequest("foo") - val requestJson = mapper.writeValueAsString(request) - - assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree("""{ "query": "foo" }""")) - - val encryptedRequest = encoderServer.encrypt(requestJson) - - wireMock.verify( - 1, - postRequestedFor(urlPathEqualTo("/query")) - .withHeader("Content-Type", equalTo("text/plain")) - .withRequestBody(equalTo(encryptedRequest)) - ) - } - } - - @Test - fun `queryInline should send correct encrypted request to lsp`() = runTest { - // use real time - withContext(Dispatchers.Default.limitedParallelism(1)) { - sut = ProjectContextProvider(project, encoderServer, this) - sut.queryInline("foo", "Foo.java", InlineContextTarget.CODEMAP) - advanceUntilIdle() - - val request = QueryInlineCompletionRequest("foo", "Foo.java", "codemap") - val requestJson = mapper.writeValueAsString(request) - - assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree("""{ "query": "foo", "filePath": "Foo.java", "target": "codemap" }""")) - - val encryptedRequest = encoderServer.encrypt(requestJson) - wireMock.verify( - 1, - postRequestedFor(urlPathEqualTo("/queryInlineProjectContext")) - .withHeader("Content-Type", equalTo("text/plain")) - .withRequestBody(equalTo(encryptedRequest)) - ) - } - } - - @Test - fun `query chat should return empty if result set non deserializable`() = runTest { - stubFor( - any(urlPathEqualTo("/query")).willReturn( - aResponse().withStatus(200).withResponseBody( - Body( - """ - [ - "foo", "bar" - ] - """.trimIndent() - ) - ) - ) - ) - - assertThrows { - sut.query("foo", null) - } - } - - @Test - fun `query chat should return deserialized relevantDocument`() = runTest { - // use real time - withContext(Dispatchers.Default.limitedParallelism(1)) { - sut = ProjectContextProvider(project, encoderServer, this) - val r = sut.query("foo", null) - advanceUntilIdle() - assertThat(r).hasSize(2) - assertThat(r[0]).isEqualTo( - RelevantDocument( - "relativeFilePath1", - "context1" - ) - ) - assertThat(r[1]).isEqualTo( - RelevantDocument( - "relativeFilePath2", - "context2" - ) - ) - } - } - - @Test - fun `query inline should throw if resultset not deserializable`() = - runTest { - sut = ProjectContextProvider(project, encoderServer, this) - stubFor( - any(urlPathEqualTo("/queryInlineProjectContext")).willReturn( - aResponse().withStatus(200).withResponseBody( - Body( - """ - [ - "foo", "bar" - ] - """.trimIndent() - ) - ) - ) - ) - - assertThrows { - withContext(getCoroutineBgContext()) { - sut.queryInline("foo", "filepath", InlineContextTarget.CODEMAP) - } - - advanceUntilIdle() - } - } - - @Test - fun `query inline should return deserialized bm25 chunks`() = runTest { - // use real time - withContext(Dispatchers.Default.limitedParallelism(1)) { - sut = ProjectContextProvider(project, encoderServer, this) - advanceUntilIdle() - val r = sut.queryInline("foo", "filepath", InlineContextTarget.CODEMAP) - assertThat(r).hasSize(3) - assertThat(r[0]).isEqualTo( - InlineBm25Chunk( - "content1", - "file1", - 0.1 - ) - ) - assertThat(r[1]).isEqualTo( - InlineBm25Chunk( - "content2", - "file2", - 0.2 - ) - ) - assertThat(r[2]).isEqualTo( - InlineBm25Chunk( - "content3", - "file3", - 0.3 - ) - ) - } - } - - @Test - fun `get usage should return memory, cpu usage`() = runTest { - val r = sut.getUsage() - assertThat(r).isEqualTo(ProjectContextProvider.Usage(123, 456)) - } - - @Test - fun `queryInline should throw if time elapsed is greater than 50ms`() = runTest { - assertThrows { - sut = ProjectContextProvider(project, encoderServer, this) - stubFor( - any(urlPathEqualTo("/queryInlineProjectContext")).willReturn( - aResponse() - .withStatus(200) - .withResponseBody( - Body(validQueryInlineResponse) - ) - .withFixedDelay(101) // 100 ms - ) - ) - - // it won't throw if it's executed within TestDispatcher context - withContext(getCoroutineBgContext()) { - sut.queryInline("foo", "bar", InlineContextTarget.CODEMAP) - } - - advanceUntilIdle() - } - } - - @Test - fun `queryChat should throw if time elapsed is greather than 500ms`() = runTest { - assertThrows { - sut = ProjectContextProvider(project, encoderServer, this) - stubFor( - any(urlPathEqualTo("/query")).willReturn( - aResponse() - .withStatus(200) - .withResponseBody( - Body(validQueryChatResponse) - ) - .withFixedDelay(501) - ) - ) - - withContext(getCoroutineBgContext()) { - sut.query("foo", timeout = 500L) - } - - advanceUntilIdle() - } - } - - @Test - fun `test index payload is encrypted`() = runTest { - whenever(encoderServer.port).thenReturn(3000) - every { sut.collectFiles() } returns FileCollectionResult( - files = listOf("Foo.java", "Bar.java", "Baz.java"), - fileSize = 10 - ) - try { - sut.index() - } catch (e: ConnectException) { - // no-op - } - verify(encoderServer, times(1)).encrypt(any()) - } - - @Test - fun `test query payload is encrypted`() = runTest { - // use real time - withContext(Dispatchers.Default.limitedParallelism(1)) { - sut = ProjectContextProvider(project, encoderServer, this) - sut.query("what does this project do", null) - advanceUntilIdle() - verify(encoderServer, times(1)).encrypt(any()) - } - } - - private fun createMockServer() = WireMockRule(wireMockConfig().dynamicPort()) -} - -// language=JSON -val validQueryInlineResponse = """ - [ - { - "content": "content1", - "filePath": "file1", - "score": 0.1 - }, - { - "content": "content2", - "filePath": "file2", - "score": 0.2 - }, - { - "content": "content3", - "filePath": "file3", - "score": 0.3 - } - ] -""".trimIndent() - -// language=JSON -val validQueryChatResponse = """ - [ - { - "filePath": "file1", - "content": "content1", - "id": "id1", - "index": "index1", - "vec": [ - "vec_1-1", - "vec_1-2", - "vec_1-3" - ], - "context": "context1", - "prev": "prev1", - "next": "next1", - "relativePath": "relativeFilePath1", - "programmingLanguage": "language1" - }, - { - "filePath": "file2", - "content": "content2", - "id": "id2", - "index": "index2", - "vec": [ - "vec_2-1", - "vec_2-2", - "vec_2-3" - ], - "context": "context2", - "prev": "prev2", - "next": "next2", - "relativePath": "relativeFilePath2", - "programmingLanguage": "language2" - } - ] -""".trimIndent() - -// language=JSON -val validGetUsageResponse = """ - { - "memoryUsage":123, - "cpuUsage":456 - } -""".trimIndent() diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt index 0ee07954037..8cf9e362fb6 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt @@ -116,8 +116,6 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.tryGetJdk import software.aws.toolkits.jetbrains.services.codemodernizer.utils.unzipFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateCustomVersionsFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateSctMetadata -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.CodeTransformPreValidationError @@ -151,7 +149,6 @@ class CodeTransformChatController( if (objective == "language upgrade" || objective == "sql conversion") { telemetry.submitSelection(objective) } - broadcastQEvent(QFeatureEvent.INVOCATION) when (objective) { "language upgrade" -> this.handleLanguageUpgrade() "sql conversion" -> this.handleSQLConversion() 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 f8a2de49534..6a2088d39c0 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 @@ -58,7 +58,6 @@ /> - diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt index 7eae90e6af0..e4755cdabee 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt @@ -34,7 +34,6 @@ class CodeWhispererRecommendationAction : AnAction(message("codewhisperer.trigge return } val latencyContext = LatencyContext() - latencyContext.codewhispererPreprocessingStart = System.nanoTime() latencyContext.codewhispererEndToEndStart = System.nanoTime() val editor = e.getRequiredData(CommonDataKeys.EDITOR) if (!( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt index ad92cde831a..148ef85ab87 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt @@ -28,6 +28,8 @@ import software.aws.toolkits.core.utils.convertMarkdownToHTML import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.ToolkitPlaces +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReferencePosition import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanHighlightingFilesPanel import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager @@ -38,8 +40,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhisp import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage 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.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_ISSUE_TITLE_MAX_LENGTH @@ -338,7 +338,6 @@ fun applySuggestedFix(project: Project, issue: CodeWhispererCodeScanIssue) { try { val manager = CodeWhispererCodeReferenceManager.getInstance(issue.project) WriteCommandAction.runWriteCommandAction(issue.project) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) val document = FileDocumentManager.getInstance().getDocument(issue.file) ?: return@runWriteCommandAction val documentContent = document.text @@ -349,9 +348,22 @@ fun applySuggestedFix(project: Project, issue: CodeWhispererCodeScanIssue) { LOG.debug { "Applied fix with reference: $reference" } val originalContent = updatedContent.substring(reference.recommendationContentSpan().start(), reference.recommendationContentSpan().end()) LOG.debug { "Original content from reference span: $originalContent" } - manager.addReferenceLogPanelEntry(reference = reference, null, null, originalContent.split("\n")) + // TODO flare: hook codescan references with flare correctly, this is only a compile error fix which is not tested + manager.addReferenceLogPanelEntry( + reference = InlineCompletionReference( + referenceName = reference.repository(), + referenceUrl = reference.url(), + licenseName = reference.licenseName(), + position = InlineCompletionReferencePosition( + startCharacter = reference.recommendationContentSpan().start(), + endCharacter = reference.recommendationContentSpan().end(), + ), + ), + null, + null, + originalContent.split("\n") + ) } - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } if (issue.suggestedFixes[0].references.isNotEmpty()) { manager.toolWindow?.show() 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 4bde496b98c..83b213ca69f 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 @@ -9,12 +9,9 @@ import com.intellij.util.text.nullize import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient import software.amazon.awssdk.services.codewhispererruntime.model.ChatInteractWithMessageEvent import software.amazon.awssdk.services.codewhispererruntime.model.ChatMessageInteractionType -import software.amazon.awssdk.services.codewhispererruntime.model.CompletionType import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse import software.amazon.awssdk.services.codewhispererruntime.model.Dimension -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisRequest import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisResponse import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobRequest @@ -22,7 +19,6 @@ import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobR import software.amazon.awssdk.services.codewhispererruntime.model.GetTestGenerationResponse import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsResponse @@ -32,31 +28,15 @@ import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnaly import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobRequest import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobResponse import software.amazon.awssdk.services.codewhispererruntime.model.StartTestGenerationResponse -import software.amazon.awssdk.services.codewhispererruntime.model.SuggestionState 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.credentials.sono.isInternalUser import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext -import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile 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 -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference -import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticDifferences -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererSuggestionState import java.time.Instant -import java.util.concurrent.TimeUnit // As the connection is project-level, we need to make this project-level too @Deprecated( @@ -66,10 +46,6 @@ import java.util.concurrent.TimeUnit interface CodeWhispererClientAdaptor { val project: Project - fun generateCompletionsPaginator( - firstRequest: GenerateCompletionsRequest, - ): Sequence - fun createUploadUrl( request: CreateUploadUrlRequest, ): CreateUploadUrlResponse @@ -84,35 +60,10 @@ interface CodeWhispererClientAdaptor { fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse - fun listAvailableCustomizations(profile: QRegionProfile): List - fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse fun getTestGeneration(jobId: String, jobGroupName: String): GetTestGenerationResponse - fun sendUserTriggerDecisionTelemetry( - requestContext: RequestContext, - responseContext: ResponseContext, - completionType: CodewhispererCompletionType, - suggestionState: CodewhispererSuggestionState, - suggestionReferenceCount: Int, - lineCount: Int, - numberOfRecommendations: Int, - acceptedCharCount: Int, - ): SendTelemetryEventResponse - - fun sendUserTriggerDecisionTelemetry( - sessionContext: SessionContextNew, - requestContext: RequestContextNew, - responseContext: ResponseContext, - completionType: CodewhispererCompletionType, - suggestionState: CodewhispererSuggestionState, - suggestionReferenceCount: Int, - lineCount: Int, - numberOfRecommendations: Int, - acceptedCharCount: Int, - ): SendTelemetryEventResponse - fun sendCodePercentageTelemetry( language: CodeWhispererProgrammingLanguage, customizationArn: String?, @@ -264,15 +215,6 @@ interface CodeWhispererClientAdaptor { 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() - do { - val response = bearerClient().generateCompletions(firstRequest.copy { it.nextToken(nextToken) }) - nextToken = response.nextToken() - yield(response) - } while (!nextToken.isNullOrEmpty()) - } - override fun createUploadUrl(request: CreateUploadUrlRequest): CreateUploadUrlResponse = bearerClient().createUploadUrl(request) @@ -287,29 +229,6 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW override fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse = bearerClient().getCodeFixJob(request) - // DO NOT directly use this method to fetch customizations, use wrapper [CodeWhispererModelConfigurator.listCustomization()] instead - override fun listAvailableCustomizations(profile: QRegionProfile): List = - QRegionProfileManager.getInstance().getQClient(project, profile).listAvailableCustomizationsPaginator( - ListAvailableCustomizationsRequest.builder().profileArn(profile.arn).build() - ) - .stream() - .toList() - .flatMap { resp -> - LOG.debug { - "listAvailableCustomizations: requestId: ${resp.responseMetadata().requestId()}, customizations: ${ - resp.customizations().map { it.name() } - }" - } - resp.customizations().map { - CodeWhispererCustomization( - arn = it.arn(), - name = it.name(), - description = it.description(), - profile = profile - ) - } - } - override fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse = bearerClient().startTestGeneration { builder -> builder.uploadId(uploadId) @@ -326,112 +245,6 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW builder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) } - override fun sendUserTriggerDecisionTelemetry( - requestContext: RequestContext, - responseContext: ResponseContext, - completionType: CodewhispererCompletionType, - suggestionState: CodewhispererSuggestionState, - suggestionReferenceCount: Int, - lineCount: Int, - numberOfRecommendations: Int, - acceptedCharCount: Int, - ): SendTelemetryEventResponse { - val fileContext = requestContext.fileContextInfo - val programmingLanguage = fileContext.programmingLanguage - var e2eLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency() - - // When we send a userTriggerDecision for neither Accept nor Reject, service side should not use this value - // and client side will set this value to 0.0. - if (suggestionState != CodewhispererSuggestionState.Accept && - suggestionState != CodewhispererSuggestionState.Reject - ) { - e2eLatency = 0.0 - } - var diffDiagnostics = DiagnosticDifferences( - added = emptyList(), - removed = emptyList() - ) - if (suggestionState == CodewhispererSuggestionState.Accept && isInternalUser(getStartUrl(project))) { - val oldDiagnostics = requestContext.diagnostics.orEmpty() - // wait for the IDE itself to update its diagnostics for current file - Thread.sleep(500) - val newDiagnostics = getDocumentDiagnostics(requestContext.editor.document, project) - diffDiagnostics = getDiagnosticDifferences(oldDiagnostics, newDiagnostics) - } - return bearerClient().sendTelemetryEvent { requestBuilder -> - requestBuilder.telemetryEvent { telemetryEventBuilder -> - telemetryEventBuilder.userTriggerDecisionEvent { - it.requestId(requestContext.latencyContext.firstRequestId) - it.completionType(completionType.toCodeWhispererSdkType()) - it.programmingLanguage { builder -> builder.languageName(programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) } - it.sessionId(responseContext.sessionId) - it.recommendationLatencyMilliseconds(e2eLatency) - it.triggerToResponseLatencyMilliseconds(requestContext.latencyContext.paginationFirstCompletionTime) - it.perceivedLatencyMilliseconds(requestContext.latencyContext.perceivedLatency) - it.suggestionState(suggestionState.toCodeWhispererSdkType()) - it.timestamp(Instant.now()) - it.suggestionReferenceCount(suggestionReferenceCount) - it.generatedLine(lineCount) - it.customizationArn(requestContext.customizationArn.nullize(nullizeSpaces = true)) - it.numberOfRecommendations(numberOfRecommendations) - it.acceptedCharacterCount(acceptedCharCount) - it.addedIdeDiagnostics(diffDiagnostics.added) - it.removedIdeDiagnostics(diffDiagnostics.removed) - } - } - requestBuilder.optOutPreference(getTelemetryOptOutPreference()) - requestBuilder.userContext(codeWhispererUserContext()) - requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) - } - } - - override fun sendUserTriggerDecisionTelemetry( - sessionContext: SessionContextNew, - requestContext: RequestContextNew, - responseContext: ResponseContext, - completionType: CodewhispererCompletionType, - suggestionState: CodewhispererSuggestionState, - suggestionReferenceCount: Int, - lineCount: Int, - numberOfRecommendations: Int, - acceptedCharCount: Int, - ): SendTelemetryEventResponse { - val fileContext = requestContext.fileContextInfo - val programmingLanguage = fileContext.programmingLanguage - var e2eLatency = sessionContext.latencyContext.getCodeWhispererEndToEndLatency() - - // When we send a userTriggerDecision of Empty or Discard, we set the time users see the first - // suggestion to be now. - if (e2eLatency < 0) { - e2eLatency = TimeUnit.NANOSECONDS.toMillis( - System.nanoTime() - sessionContext.latencyContext.codewhispererEndToEndStart - ).toDouble() - } - return bearerClient().sendTelemetryEvent { requestBuilder -> - requestBuilder.telemetryEvent { telemetryEventBuilder -> - telemetryEventBuilder.userTriggerDecisionEvent { - it.requestId(sessionContext.latencyContext.firstRequestId) - it.completionType(completionType.toCodeWhispererSdkType()) - it.programmingLanguage { builder -> builder.languageName(programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) } - it.sessionId(responseContext.sessionId) - it.recommendationLatencyMilliseconds(e2eLatency) - it.triggerToResponseLatencyMilliseconds(sessionContext.latencyContext.paginationFirstCompletionTime) - it.perceivedLatencyMilliseconds(sessionContext.latencyContext.perceivedLatency) - it.suggestionState(suggestionState.toCodeWhispererSdkType()) - it.timestamp(Instant.now()) - it.suggestionReferenceCount(suggestionReferenceCount) - it.generatedLine(lineCount) - it.customizationArn(requestContext.customizationArn.nullize(nullizeSpaces = true)) - it.numberOfRecommendations(numberOfRecommendations) - it.acceptedCharacterCount(acceptedCharCount) - } - } - requestBuilder.optOutPreference(getTelemetryOptOutPreference()) - requestBuilder.userContext(codeWhispererUserContext()) - requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) - } - } - override fun sendCodePercentageTelemetry( language: CodeWhispererProgrammingLanguage, customizationArn: String?, @@ -818,22 +631,4 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW requestBuilder.userContext(codeWhispererUserContext()) requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) } - - companion object { - private val LOG = getLogger() - } -} - -private fun CodewhispererSuggestionState.toCodeWhispererSdkType() = when { - this == CodewhispererSuggestionState.Accept -> SuggestionState.ACCEPT - this == CodewhispererSuggestionState.Reject -> SuggestionState.REJECT - this == CodewhispererSuggestionState.Empty -> SuggestionState.EMPTY - this == CodewhispererSuggestionState.Discard -> SuggestionState.DISCARD - else -> SuggestionState.UNKNOWN_TO_SDK_VERSION -} - -private fun CodewhispererCompletionType.toCodeWhispererSdkType() = when { - this == CodewhispererCompletionType.Line -> CompletionType.LINE - this == CodewhispererCompletionType.Block -> CompletionType.BLOCK - else -> CompletionType.UNKNOWN_TO_SDK_VERSION } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt deleted file mode 100644 index e8f28844972..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.editor - -import com.intellij.openapi.editor.event.BulkAwareDocumentListener -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.editor.event.EditorFactoryEvent -import com.intellij.openapi.editor.event.EditorFactoryListener -import com.intellij.openapi.editor.impl.EditorImpl -import com.intellij.openapi.fileEditor.FileDocumentManager -import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService -import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker - -class CodeWhispererEditorListener : EditorFactoryListener { - override fun editorCreated(event: EditorFactoryEvent) { - val editor = (event.editor as? EditorImpl) ?: return - val project = editor.project ?: return - - val language = FileDocumentManager.getInstance().getFile(editor.document)?.programmingLanguage() ?: return - // If language is not supported by CodeWhisperer, no action needed - if (!language.isCodeCompletionSupported()) return - // If language is supported, install document listener for CodeWhisperer service - editor.document.addDocumentListener( - object : BulkAwareDocumentListener { - // TODO: Track only deletion changes within the current 5-min interval which will give - // the most accurate code percentage data. - override fun documentChanged(event: DocumentEvent) { - if (!isCodeWhispererEnabled(project)) return - if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) { - CodeWhispererInvocationStatusNew.getInstance().documentChanged() - } else { - CodeWhispererInvocationStatus.getInstance().documentChanged() - } - CodeWhispererCodeCoverageTracker.getInstance(project, language).apply { - activateTrackerIfNotActive() - documentChanged(event) - } - UserWrittenCodeTracker.getInstance(project).apply { - activateTrackerIfNotActive() - documentChanged(event) - } - } - }, - editor.disposable - ) - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt index 7da7a282e9f..5b1e3320f82 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt @@ -12,27 +12,24 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager +import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_QUOTES import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.resources.message -import java.time.Instant import java.util.Stack @Service class CodeWhispererEditorManager { fun updateEditorWithRecommendation(states: InvocationContext, sessionContext: SessionContext) { - val (requestContext, responseContext, recommendationContext) = states + val (requestContext, _, recommendationContext) = states val (project, editor) = requestContext val document = editor.document val primaryCaret = editor.caretModel.primaryCaret @@ -41,7 +38,7 @@ class CodeWhispererEditorManager { val detail = recommendationContext.details[selectedIndex] val reformatted = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( detail, - recommendationContext.userInputSinceInvocation + recommendationContext.userInput ) val remainingRecommendation = reformatted.substring(typeahead.length) val originalOffset = primaryCaret.offset - typeahead.length @@ -51,31 +48,18 @@ class CodeWhispererEditorManager { val insertEndOffset = sessionContext.insertEndOffset val endOffsetToReplace = if (insertEndOffset != -1) insertEndOffset else primaryCaret.offset + detail.isAccepted = true + WriteCommandAction.runWriteCommandAction(project) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) document.replaceString(originalOffset, endOffsetToReplace, reformatted) PsiDocumentManager.getInstance(project).commitDocument(document) - primaryCaret.moveToOffset(endOffset + detail.rightOverlap.length) - - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) + primaryCaret.moveToOffset(endOffset) } ApplicationManager.getApplication().invokeLater { WriteCommandAction.runWriteCommandAction(project) { val rangeMarker = document.createRangeMarker(originalOffset, endOffset, true) - CodeWhispererTelemetryService.getInstance().enqueueAcceptedSuggestionEntry( - detail.requestId, - requestContext, - responseContext, - Instant.now(), - PsiDocumentManager.getInstance(project).getPsiFile(document)?.virtualFile, - rangeMarker, - remainingRecommendation, - selectedIndex, - detail.completionType - ) - ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED, ).afterAccept(states, sessionContext, rangeMarker) @@ -83,7 +67,9 @@ class CodeWhispererEditorManager { } // Display tab accept priority once when the first accept is made - if (!CodeWhispererSettings.getInstance().isTabAcceptPriorityNotificationShownOnce()) { + if (!CodeWhispererSettings.getInstance().isTabAcceptPriorityNotificationShownOnce() && + CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX() + ) { notifyInfo( "Amazon Q", message("codewhisperer.inline.settings.tab_priority.notification.text"), @@ -125,7 +111,6 @@ class CodeWhispererEditorManager { fun getMatchingSymbolsFromRecommendation( editor: Editor, recommendation: String, - isTruncatedOnRight: Boolean, sessionContext: SessionContext, ): List> { val result = mutableListOf>() @@ -142,8 +127,6 @@ class CodeWhispererEditorManager { result.add(0 to caretOffset) result.add(recommendation.length + 1 to lineEndOffset) - if (isTruncatedOnRight) return result - while (current < recommendation.length && totalDocLengthChecked < lineText.length && totalDocLengthChecked < recommendation.length @@ -225,16 +208,9 @@ class CodeWhispererEditorManager { fun findOverLappingLines( editor: Editor, recommendationLines: List, - isTruncatedOnRight: Boolean, sessionContext: SessionContext, ): Int { val caretOffset = editor.caretModel.offset - if (isTruncatedOnRight) { - // insertEndOffset value only makes sense when there are matching closing brackets, if there's right context - // resolution applied, set this value to the current caret offset - sessionContext.insertEndOffset = caretOffset - return 0 - } val text = editor.document.charsSequence val document = editor.document diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt index 8e590fbba3b..c8f217fa4ce 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt @@ -15,13 +15,9 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionConte import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_QUOTES -import java.time.Instant import java.util.Stack @Service @@ -31,7 +27,6 @@ class CodeWhispererEditorManagerNew { val selectedIndex = sessionContext.selectedIndex val preview = previews[selectedIndex] val states = CodeWhispererServiceNew.getInstance().getAllPaginationSessions()[preview.jobId] ?: return - val (requestContext, responseContext) = states val (project, editor) = sessionContext val document = editor.document val primaryCaret = editor.caretModel.primaryCaret @@ -53,30 +48,15 @@ class CodeWhispererEditorManagerNew { preview.detail.isAccepted = true WriteCommandAction.runWriteCommandAction(project) { - broadcastQEvent(QFeatureEvent.STARTS_EDITING) document.replaceString(originalOffset, endOffsetToReplace, reformatted) PsiDocumentManager.getInstance(project).commitDocument(document) - primaryCaret.moveToOffset(endOffset + detail.rightOverlap.length) - - broadcastQEvent(QFeatureEvent.FINISHES_EDITING) + primaryCaret.moveToOffset(endOffset) } ApplicationManager.getApplication().invokeLater { WriteCommandAction.runWriteCommandAction(project) { val rangeMarker = document.createRangeMarker(originalOffset, endOffset, true) - CodeWhispererTelemetryServiceNew.getInstance().enqueueAcceptedSuggestionEntry( - detail.requestId, - requestContext, - responseContext, - Instant.now(), - PsiDocumentManager.getInstance(project).getPsiFile(document)?.virtualFile, - rangeMarker, - remainingRecommendation, - selectedIndex, - detail.completionType - ) - ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED, ).afterAccept(states, previews, sessionContext, rangeMarker) @@ -106,7 +86,6 @@ class CodeWhispererEditorManagerNew { fun getMatchingSymbolsFromRecommendation( editor: Editor, recommendation: String, - isTruncatedOnRight: Boolean, sessionContext: SessionContextNew, ): List> { val result = mutableListOf>() @@ -124,8 +103,6 @@ class CodeWhispererEditorManagerNew { result.add(0 to caretOffset) result.add(recommendation.length + 1 to lineEndOffset) - if (isTruncatedOnRight) return result - while (current < recommendation.length && totalDocLengthChecked < lineText.length && totalDocLengthChecked < recommendation.length @@ -208,17 +185,9 @@ class CodeWhispererEditorManagerNew { fun findOverLappingLines( editor: Editor, recommendationLines: List, - isTruncatedOnRight: Boolean, sessionContext: SessionContextNew, ): Int { val caretOffset = editor.caretModel.offset - if (isTruncatedOnRight) { - // insertEndOffset value only makes sense when there are matching closing brackets, if there's right context - // resolution applied, set this value to the current caret offset - sessionContext.insertEndOffset = caretOffset - return 0 - } - val text = editor.document.charsSequence val document = editor.document val textLines = mutableListOf>() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt index 61b79766881..0d8406488d9 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt @@ -101,17 +101,6 @@ object CodeWhispererEditorUtil { ) } - fun shouldSkipInvokingBasedOnRightContext(editor: Editor): Boolean { - val caretContext = runReadAction { extractCaretContext(editor) } - val rightContextLines = caretContext.rightFileContext.split(Regex("\r?\n")) - val rightContextCurrentLine = if (rightContextLines.isEmpty()) "" else rightContextLines[0] - - return rightContextCurrentLine.isNotEmpty() && - !rightContextCurrentLine.startsWith(" ") && - rightContextCurrentLine.trim() != ("}") && - rightContextCurrentLine.trim() != (")") - } - /** * Check if left context contains keywords or file name follow config json file naming pattern */ diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEnterHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEnterHandler.kt index 8d789f98e3f..fbaec53e3d3 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEnterHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEnterHandler.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.shouldSkipInvokingBasedOnRightContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread @@ -17,12 +16,8 @@ class CodeWhispererEnterHandler(private val originalHandler: EditorActionHandler override fun executeWriteAction(editor: Editor, caret: Caret?, dataContext: DataContext?) { originalHandler.execute(editor, caret, dataContext) - if (shouldSkipInvokingBasedOnRightContext(editor)) { - return - } - pluginAwareExecuteOnPooledThread { - CodeWhispererAutoTriggerService.getInstance().tryInvokeAutoTrigger(editor, CodeWhispererAutomatedTriggerType.Enter()) + CodeWhispererAutoTriggerService.getInstance().invoke(editor, CodeWhispererAutomatedTriggerType.Enter()) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererTypedHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererTypedHandler.kt index 0cc5d929501..e585bc5d486 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererTypedHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererTypedHandler.kt @@ -7,24 +7,19 @@ import com.intellij.codeInsight.editorActions.TypedHandlerDelegate import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.shouldSkipInvokingBasedOnRightContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants class CodeWhispererTypedHandler : TypedHandlerDelegate() { override fun charTyped(c: Char, project: Project, editor: Editor, psiFiles: PsiFile): Result { - if (shouldSkipInvokingBasedOnRightContext(editor)) { - return Result.CONTINUE - } - // Special Char if (CodeWhispererConstants.SPECIAL_CHARACTERS_LIST.contains(c.toString())) { - CodeWhispererAutoTriggerService.getInstance().tryInvokeAutoTrigger(editor, CodeWhispererAutomatedTriggerType.SpecialChar(c)) + CodeWhispererAutoTriggerService.getInstance().invoke(editor, CodeWhispererAutomatedTriggerType.SpecialChar(c)) return Result.CONTINUE } - CodeWhispererAutoTriggerService.getInstance().tryInvokeAutoTrigger(editor, CodeWhispererAutomatedTriggerType.Classifier()) + CodeWhispererAutoTriggerService.getInstance().invoke(editor, CodeWhispererAutomatedTriggerType.Classifier()) return Result.CONTINUE } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdder.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdder.kt index 0d4f2873727..623395afe49 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdder.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdder.kt @@ -8,10 +8,10 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile -import software.amazon.awssdk.services.codewhispererruntime.model.Import 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.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionImports import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew @@ -24,29 +24,29 @@ abstract class CodeWhispererImportAdder { abstract val dummyFileName: String fun insertImportStatements(states: InvocationContext, sessionContext: SessionContext) { - val imports = states.recommendationContext.details[sessionContext.selectedIndex] - .recommendation.mostRelevantMissingImports() - LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" } - imports.forEach { + val completion = states.recommendationContext.details[sessionContext.selectedIndex].completion + val imports = completion.mostRelevantMissingImports + LOG.info { "Adding ${imports?.size ?: 0} imports for completions, sessionId: ${states.responseContext.sessionId}" } + imports?.forEach { insertImportStatement(states, it) } } fun insertImportStatements(states: InvocationContextNew, previews: List, sessionContext: SessionContextNew) { - val imports = previews[sessionContext.selectedIndex].detail.recommendation.mostRelevantMissingImports() - LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" } - imports.forEach { + val imports = previews[sessionContext.selectedIndex].detail.completion.mostRelevantMissingImports + LOG.info { "Adding ${imports?.size ?: 0} imports for completions, sessionId: ${states.responseContext.sessionId}" } + imports?.forEach { insertImportStatement(states, it) } } - private fun insertImportStatement(states: InvocationContext, import: Import) { + private fun insertImportStatement(states: InvocationContext, import: InlineCompletionImports) { val project = states.requestContext.project val editor = states.requestContext.editor val document = editor.document val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return - val statement = import.statement() + val statement = import.statement LOG.info { "Import statement to be added: $statement" } val newImport = createNewImportPsiElement(psiFile, statement) if (newImport == null) { @@ -72,13 +72,13 @@ abstract class CodeWhispererImportAdder { LOG.info { "Added import: $added" } } - private fun insertImportStatement(states: InvocationContextNew, import: Import) { + private fun insertImportStatement(states: InvocationContextNew, import: InlineCompletionImports) { val project = states.requestContext.project val editor = states.requestContext.editor val document = editor.document val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return - val statement = import.statement() + val statement = import.statement LOG.info { "Import statement to be added: $statement" } val newImport = createNewImportPsiElement(psiFile, statement) if (newImport == null) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdderListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdderListener.kt index a5abde5d9b2..3e8721e753b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdderListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdderListener.kt @@ -22,10 +22,6 @@ object CodeWhispererImportAdderListener : CodeWhispererUserActionListener { return } val language = states.requestContext.fileContextInfo.programmingLanguage - if (!language.isImportAdderSupported()) { - LOG.debug { "Import adder is not supported for $language" } - return - } val importAdder = CodeWhispererImportAdder.get(language) if (importAdder == null) { LOG.debug { "No import adder found for $language" } @@ -40,10 +36,6 @@ object CodeWhispererImportAdderListener : CodeWhispererUserActionListener { return } val language = states.requestContext.fileContextInfo.programmingLanguage - if (!language.isImportAdderSupported()) { - LOG.debug { "Import adder is not supported for $language" } - return - } val importAdder = CodeWhispererImportAdder.get(language) if (importAdder == null) { LOG.debug { "No import adder found for $language" } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt index 98e5486cd2a..3458724b2fc 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt @@ -3,8 +3,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.NoOpFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage /** @@ -18,20 +16,11 @@ import software.aws.toolkits.telemetry.CodewhispererLanguage */ abstract class CodeWhispererProgrammingLanguage { abstract val languageId: String - open val fileCrawler: FileCrawler = NoOpFileCrawler() abstract fun toTelemetryType(): CodewhispererLanguage - open fun isCodeCompletionSupported(): Boolean = false - open fun isAutoFileScanSupported(): Boolean = false - open fun isImportAdderSupported(): Boolean = false - - open fun isSupplementalContextSupported(): Boolean = false - - open fun isUTGSupported(): Boolean = false - open fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = this open fun lineCommentPrefix(): String? = "//" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererAbap.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererAbap.kt index da8334406a4..4096e519e24 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererAbap.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererAbap.kt @@ -11,8 +11,6 @@ class CodeWhispererAbap private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Abap - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "abap" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererC.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererC.kt index e49ed2b606b..79c2bc56b1a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererC.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererC.kt @@ -11,8 +11,6 @@ class CodeWhispererC private constructor() : CodeWhispererProgrammingLanguage() override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.C - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCpp.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCpp.kt index ad59afb74ea..7ea3c185d75 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCpp.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCpp.kt @@ -11,8 +11,6 @@ class CodeWhispererCpp private constructor() : CodeWhispererProgrammingLanguage( override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Cpp - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCsharp.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCsharp.kt index 094790065be..b9edf0937d2 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCsharp.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererCsharp.kt @@ -11,8 +11,6 @@ class CodeWhispererCsharp private constructor() : CodeWhispererProgrammingLangua override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Csharp - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererDart.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererDart.kt index 7ba4d022cb3..dc60a955067 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererDart.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererDart.kt @@ -11,8 +11,6 @@ class CodeWhispererDart private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Dart - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "dart" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererGo.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererGo.kt index 336c7d79447..b163a69b405 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererGo.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererGo.kt @@ -11,8 +11,6 @@ class CodeWhispererGo private constructor() : CodeWhispererProgrammingLanguage() override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Go - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJava.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJava.kt index 0af1de36ed0..0b66e5828ec 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJava.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJava.kt @@ -4,26 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.JavaCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererJava private constructor() : CodeWhispererProgrammingLanguage() { override val languageId: String = ID - override val fileCrawler: FileCrawler = JavaCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Java - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true - override fun isImportAdderSupported(): Boolean = true - - override fun isSupplementalContextSupported() = true - - override fun isUTGSupported() = true - companion object { const val ID = "java" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJavaScript.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJavaScript.kt index a5d03bb2c64..b906ec2676a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJavaScript.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJavaScript.kt @@ -4,24 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.JavascriptCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererJavaScript private constructor() : CodeWhispererProgrammingLanguage() { override val languageId: String = ID - override val fileCrawler: FileCrawler = JavascriptCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Javascript - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true - override fun isImportAdderSupported(): Boolean = true - - override fun isSupplementalContextSupported() = true - companion object { const val ID = "javascript" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt index aba531eb64e..faa63692527 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt @@ -11,8 +11,6 @@ class CodeWhispererJson private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Json - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true override fun lineCommentPrefix() = null diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJsx.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJsx.kt index 6039b31815d..0c2e35f8edd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJsx.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJsx.kt @@ -4,22 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.JavascriptCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererJsx private constructor() : CodeWhispererProgrammingLanguage() { override val languageId: String = ID - override val fileCrawler: FileCrawler = JavascriptCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Jsx - override fun isCodeCompletionSupported(): Boolean = true - override fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererJavaScript.INSTANCE - override fun isSupplementalContextSupported() = true - companion object { const val ID = "jsx" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererKotlin.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererKotlin.kt index 0edb7042c1e..e7b17411842 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererKotlin.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererKotlin.kt @@ -11,8 +11,6 @@ class CodeWhispererKotlin private constructor() : CodeWhispererProgrammingLangua override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Kotlin - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "kotlin" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererLua.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererLua.kt index 5b2f39d73e7..fbf509536b7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererLua.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererLua.kt @@ -11,8 +11,6 @@ class CodeWhispererLua private constructor() : CodeWhispererProgrammingLanguage( override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Lua - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "lua" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPhp.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPhp.kt index 026b44a304a..f03c8b837aa 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPhp.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPhp.kt @@ -11,8 +11,6 @@ class CodeWhispererPhp private constructor() : CodeWhispererProgrammingLanguage( override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Php - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPowershell.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPowershell.kt index e74e7153c45..7f744b51de7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPowershell.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPowershell.kt @@ -11,8 +11,6 @@ class CodeWhispererPowershell private constructor() : CodeWhispererProgrammingLa override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Powershell - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "powershell" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt index c2b4ce25f48..7a32ce4f382 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt @@ -4,26 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.PythonCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererPython private constructor() : CodeWhispererProgrammingLanguage() { override val languageId = ID - override val fileCrawler: FileCrawler = PythonCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Python - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true - override fun isImportAdderSupported(): Boolean = true - - override fun isUTGSupported() = true - - override fun isSupplementalContextSupported() = true - override fun lineCommentPrefix() = "#" override fun blockCommentPrefix() = "\"\"\"" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererR.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererR.kt index 5444724347b..c3fe1d098dc 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererR.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererR.kt @@ -11,8 +11,6 @@ class CodeWhispererR private constructor() : CodeWhispererProgrammingLanguage() override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.R - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "r" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt index aef64cac8ff..d4da9b58b2b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt @@ -11,8 +11,6 @@ class CodeWhispererRuby private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Ruby - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true override fun lineCommentPrefix(): String = "#" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRust.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRust.kt index cf953ff1da2..ca53cb6488a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRust.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRust.kt @@ -11,8 +11,6 @@ class CodeWhispererRust private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Rust - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "rust" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererScala.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererScala.kt index ffd5594706a..16308f04f4a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererScala.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererScala.kt @@ -11,8 +11,6 @@ class CodeWhispererScala private constructor() : CodeWhispererProgrammingLanguag override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Scala - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "scala" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt index a096ff1bb12..55a77ea63aa 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt @@ -11,8 +11,6 @@ class CodeWhispererShell private constructor() : CodeWhispererProgrammingLanguag override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Shell - override fun isCodeCompletionSupported(): Boolean = true - override fun lineCommentPrefix(): String = "#" override fun blockCommentPrefix(): String = ": '" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSql.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSql.kt index f316c1c375f..ee1893ae4ed 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSql.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSql.kt @@ -11,8 +11,6 @@ class CodeWhispererSql private constructor() : CodeWhispererProgrammingLanguage( override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Sql - override fun isCodeCompletionSupported(): Boolean = true - companion object { const val ID = "sql" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSwift.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSwift.kt index 36513f8908e..3635e7e6573 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSwift.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSwift.kt @@ -11,8 +11,6 @@ class CodeWhispererSwift private constructor() : CodeWhispererProgrammingLanguag override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Swift - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "swift" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSystemVerilog.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSystemVerilog.kt index 4f445ebffad..a94ab848889 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSystemVerilog.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererSystemVerilog.kt @@ -11,8 +11,6 @@ class CodeWhispererSystemVerilog private constructor() : CodeWhispererProgrammin override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.SystemVerilog - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "systemverilog" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt index 0e5123b3cce..19cfbbff0fb 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt @@ -11,8 +11,6 @@ class CodeWhispererTf private constructor() : CodeWhispererProgrammingLanguage() override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Tf - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true override fun lineCommentPrefix(): String = "#" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTsx.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTsx.kt index 4334d3b2333..6a07870b537 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTsx.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTsx.kt @@ -4,22 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.TypescriptCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererTsx private constructor() : CodeWhispererProgrammingLanguage() { override val languageId: String = ID - override val fileCrawler: FileCrawler = TypescriptCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Tsx - override fun isCodeCompletionSupported(): Boolean = true - override fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererTypeScript.INSTANCE - override fun isSupplementalContextSupported() = true - companion object { const val ID = "tsx" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTypeScript.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTypeScript.kt index 88545b97db8..c59a2cf5981 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTypeScript.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTypeScript.kt @@ -4,22 +4,15 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.TypescriptCodeWhispererFileCrawler import software.aws.toolkits.telemetry.CodewhispererLanguage class CodeWhispererTypeScript private constructor() : CodeWhispererProgrammingLanguage() { override val languageId: String = ID - override val fileCrawler: FileCrawler = TypescriptCodeWhispererFileCrawler override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Typescript - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true - override fun isSupplementalContextSupported() = true - companion object { const val ID = "typescript" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererVue.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererVue.kt index a2345178fe2..09ed306a074 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererVue.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererVue.kt @@ -11,8 +11,6 @@ class CodeWhispererVue private constructor() : CodeWhispererProgrammingLanguage( override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Vue - override fun isCodeCompletionSupported(): Boolean = true - companion object { // TODO: confirm with service team language id const val ID = "vue" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt index c110d797cb0..b979848ceac 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt @@ -11,8 +11,6 @@ class CodeWhispererYaml private constructor() : CodeWhispererProgrammingLanguage override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Yaml - override fun isCodeCompletionSupported(): Boolean = true - override fun isAutoFileScanSupported(): Boolean = true override fun lineCommentPrefix(): String = "#" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt index fdf4c4e7377..61157a2a1d1 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt @@ -10,12 +10,10 @@ import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.concurrency.annotations.RequiresEdt -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection -import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionItem +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew @@ -30,14 +28,9 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseCo import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.setIntelliSensePopupAlpha -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy -import software.aws.toolkits.jetbrains.services.codewhisperer.util.SupplementalContextStrategy -import software.aws.toolkits.jetbrains.services.codewhisperer.util.UtgStrategy import software.aws.toolkits.telemetry.CodewhispererCompletionType import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Result -import java.time.Duration -import java.time.Instant import java.util.concurrent.TimeUnit data class Chunk( @@ -47,11 +40,6 @@ data class Chunk( val score: Double = 0.0, ) -data class ListUtgCandidateResult( - val vfile: VirtualFile?, - val strategy: UtgStrategy, -) - data class CaretContext(val leftFileContext: String, val rightFileContext: String, val leftContextOnCurrentLine: String = "") data class FileContextInfo( @@ -62,51 +50,15 @@ data class FileContextInfo( val fileUri: String?, ) -data class SupplementalContextInfo( - val isUtg: Boolean, - val contents: List, - val targetFileName: String, - val strategy: SupplementalContextStrategy, - val latency: Long = 0L, -) { - val contentLength: Int - get() = contents.fold(0) { acc, chunk -> - acc + chunk.content.length - } - - val isProcessTimeout: Boolean - get() = latency > SUPPLEMENTAL_CONTEXT_TIMEOUT - - companion object { - fun emptyCrossFileContextInfo(targetFileName: String): SupplementalContextInfo = SupplementalContextInfo( - isUtg = false, - contents = emptyList(), - targetFileName = targetFileName, - strategy = CrossFileStrategy.Empty, - latency = 0L - ) - - fun emptyUtgFileContextInfo(targetFileName: String): SupplementalContextInfo = SupplementalContextInfo( - isUtg = true, - contents = emptyList(), - targetFileName = targetFileName, - strategy = UtgStrategy.Empty, - latency = 0L - ) - } -} - data class RecommendationContext( val details: List, - val userInputOriginal: String, - val userInputSinceInvocation: String, + val userInput: String, val position: VisualPosition, ) data class RecommendationContextNew( - val details: MutableList, - val userInputOriginal: String, - val userInputSinceInvocation: String, + val details: MutableList, + val userInput: String, val position: VisualPosition, val jobId: Int, var typeahead: String = "", @@ -114,28 +66,15 @@ data class RecommendationContextNew( data class PreviewContext( val jobId: Int, - val detail: DetailContextNew, + val detail: DetailContext, val userInput: String, val typeahead: String, ) data class DetailContext( - val requestId: String, - val recommendation: Completion, - val reformatted: Completion, + val itemId: String, + val completion: InlineCompletionItem, val isDiscarded: Boolean, - val isTruncatedOnRight: Boolean, - val rightOverlap: String = "", - val completionType: CodewhispererCompletionType, -) - -data class DetailContextNew( - val requestId: String, - val recommendation: Completion, - val reformatted: Completion, - val isDiscarded: Boolean, - val isTruncatedOnRight: Boolean, - val rightOverlap: String = "", val completionType: CodewhispererCompletionType, var hasSeen: Boolean = false, var isAccepted: Boolean = false, @@ -149,7 +88,6 @@ data class SessionContext( var toBeRemovedHighlighter: RangeHighlighter? = null, var insertEndOffset: Int = -1, var isPopupShowing: Boolean = false, - var perceivedLatency: Double = -1.0, ) data class SessionContextNew( @@ -178,11 +116,7 @@ data class SessionContextNew( @RequiresEdt override fun dispose() { - CodeWhispererTelemetryServiceNew.getInstance().sendUserDecisionEventForAll( - this, - hasAccepted, - CodeWhispererInvocationStatusNew.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) } - ) + CodeWhispererTelemetryServiceNew.getInstance().sendUserTriggerDecisionEvent(this.project, this.latencyContext) setIntelliSensePopupAlpha(editor, 0f) CodeWhispererInvocationStatusNew.getInstance().setDisplaySessionActive(false) @@ -239,14 +173,14 @@ data class InvocationContextNew( data class WorkerContext( val requestContext: RequestContext, val responseContext: ResponseContext, - val response: GenerateCompletionsResponse, + val completions: InlineCompletionListWithReferences, val popup: JBPopup, ) data class WorkerContextNew( val requestContext: RequestContextNew, val responseContext: ResponseContext, - val response: GenerateCompletionsResponse, + val completions: InlineCompletionListWithReferences, ) data class CodeScanTelemetryEvent( @@ -274,46 +208,17 @@ data class CodeScanResponseContext( ) data class LatencyContext( - var credentialFetchingStart: Long = 0L, - var credentialFetchingEnd: Long = 0L, - - var codewhispererPreprocessingStart: Long = 0L, - var codewhispererPreprocessingEnd: Long = 0L, - - var paginationFirstCompletionTime: Double = 0.0, var perceivedLatency: Double = 0.0, - var codewhispererPostprocessingStart: Long = 0L, - var codewhispererPostprocessingEnd: Long = 0L, - var codewhispererEndToEndStart: Long = 0L, var codewhispererEndToEndEnd: Long = 0L, - var paginationAllCompletionsStart: Long = 0L, - var paginationAllCompletionsEnd: Long = 0L, - var firstRequestId: String = "", ) { fun getCodeWhispererEndToEndLatency() = TimeUnit.NANOSECONDS.toMillis( codewhispererEndToEndEnd - codewhispererEndToEndStart ).toDouble() - fun getCodeWhispererAllCompletionsLatency() = TimeUnit.NANOSECONDS.toMillis( - paginationAllCompletionsEnd - paginationAllCompletionsStart - ).toDouble() - - fun getCodeWhispererPostprocessingLatency() = TimeUnit.NANOSECONDS.toMillis( - codewhispererPostprocessingEnd - codewhispererPostprocessingStart - ).toDouble() - - fun getCodeWhispererCredentialFetchingLatency() = TimeUnit.NANOSECONDS.toMillis( - credentialFetchingEnd - credentialFetchingStart - ).toDouble() - - fun getCodeWhispererPreprocessingLatency() = TimeUnit.NANOSECONDS.toMillis( - codewhispererPreprocessingEnd - codewhispererPreprocessingStart - ).toDouble() - // For auto-trigger it's from the time when last char typed // for manual-trigger it's from the time when last trigger action happened(alt + c) fun getPerceivedLatency(triggerType: CodewhispererTriggerType) = diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt index f452de9a00d..4e1f65c6f51 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt @@ -9,27 +9,22 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationCo import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import java.time.Duration -import java.time.Instant class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopupListener { override fun beforeShown(event: LightweightWindowEvent) { super.beforeShown(event) - CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp() + CodeWhispererInvocationStatus.getInstance().completionShown() } override fun onClosed(event: LightweightWindowEvent) { super.onClosed(event) val (requestContext, responseContext, recommendationContext) = states - CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll( - requestContext, - responseContext, - recommendationContext, - CodeWhispererPopupManager.getInstance().sessionContext, - event.isOk, - CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) } + CodeWhispererTelemetryService.getInstance().sendUserTriggerDecisionEvent( + requestContext.project, + requestContext.latencyContext, + responseContext.sessionId, + recommendationContext ) - CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt index 234813002bb..e7979dcbbe0 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt @@ -40,10 +40,10 @@ import com.intellij.ui.popup.AbstractPopup import com.intellij.ui.popup.PopupFactoryImpl import com.intellij.util.messages.Topic import com.intellij.util.ui.UIUtil -import software.amazon.awssdk.services.codewhispererruntime.model.Import -import software.amazon.awssdk.services.codewhispererruntime.model.Reference import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionImports +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints @@ -65,7 +65,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.Co import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererPrevButtonActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE @@ -131,8 +130,7 @@ class CodeWhispererPopupManager { .recommendationAdded(states, sessionContext) return } - val userInputOriginal = recommendationContext.userInputOriginal - val userInput = recommendationContext.userInputSinceInvocation + val userInput = recommendationContext.userInput val typeaheadOriginal = run { val startOffset = states.requestContext.caretPosition.offset val currOffset = states.requestContext.editor.caretModel.offset @@ -144,11 +142,11 @@ class CodeWhispererPopupManager { // userInput + typeahead val prefix = states.requestContext.editor.document.charsSequence .substring(startOffset, currOffset) - if (prefix.length < userInputOriginal.length) { + if (prefix.length < userInput.length) { cancelPopup(popup) return } else { - prefix.substring(userInputOriginal.length) + prefix.substring(userInput.length) } } val isReverse = indexChange < 0 @@ -178,8 +176,7 @@ class CodeWhispererPopupManager { selectedIndex, sessionContext.seen, sessionContext.toBeRemovedHighlighter, - isPopupShowing = sessionContext.isPopupShowing, - perceivedLatency = sessionContext.perceivedLatency + isPopupShowing = sessionContext.isPopupShowing ) ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged( @@ -189,8 +186,8 @@ class CodeWhispererPopupManager { } private fun resolveTypeahead(states: InvocationContext, selectedIndex: Int, typeahead: String): String { - val recommendation = states.recommendationContext.details[selectedIndex].reformatted.content() - val userInput = states.recommendationContext.userInputSinceInvocation + val recommendation = states.recommendationContext.details[selectedIndex].completion.insertText + val userInput = states.recommendationContext.userInput var indexOfFirstNonWhiteSpace = typeahead.indexOfFirst { !it.isWhitespace() } if (indexOfFirstNonWhiteSpace == -1) { indexOfFirstNonWhiteSpace = typeahead.length @@ -204,7 +201,7 @@ class CodeWhispererPopupManager { } fun updatePopupPanel(states: InvocationContext, sessionContext: SessionContext) { - val userInput = states.recommendationContext.userInputSinceInvocation + val userInput = states.recommendationContext.userInput val details = states.recommendationContext.details val selectedIndex = sessionContext.selectedIndex val typeaheadOriginal = sessionContext.typeaheadOriginal @@ -212,8 +209,8 @@ class CodeWhispererPopupManager { val validSelectedIndex = getValidSelectedIndex(details, userInput, selectedIndex, typeaheadOriginal) updateSelectedRecommendationLabelText(validSelectedIndex, validCount) updateNavigationPanel(validSelectedIndex, validCount) - updateImportPanel(details[selectedIndex].recommendation.mostRelevantMissingImports()) - updateCodeReferencePanel(states.requestContext.project, details[selectedIndex].recommendation.references()) + updateImportPanel(details[selectedIndex].completion.mostRelevantMissingImports) + updateCodeReferencePanel(states.requestContext.project, details[selectedIndex].completion.references) } fun render( @@ -236,7 +233,6 @@ class CodeWhispererPopupManager { // 4. User navigating through the completions or typing as the completion shows. We should not update the latency // end time and should not emit any events in this case. if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) { - states.requestContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime() states.requestContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime() states.requestContext.latencyContext.perceivedLatency = states.requestContext.latencyContext.getPerceivedLatency(states.requestContext.triggerTypeInfo.triggerType) @@ -286,12 +282,11 @@ class CodeWhispererPopupManager { val caretPoint = states.requestContext.editor.offsetToXY(states.requestContext.caretPosition.offset) val editor = states.requestContext.editor val detailContexts = states.recommendationContext.details - val userInputOriginal = states.recommendationContext.userInputOriginal - val userInput = states.recommendationContext.userInputSinceInvocation + val userInput = states.recommendationContext.userInput val selectedIndex = sessionContext.selectedIndex val typeaheadOriginal = sessionContext.typeaheadOriginal val typeahead = sessionContext.typeahead - val userInputLines = userInputOriginal.split("\n").size - 1 + val userInputLines = userInput.split("\n").size - 1 val lineCount = getReformattedRecommendation(detailContexts[selectedIndex], userInput).split("\n").size val additionalLines = typeaheadOriginal.split("\n").size - typeahead.split("\n").size val popupSize = (popup as AbstractPopup).preferredContentSize @@ -364,17 +359,6 @@ class CodeWhispererPopupManager { editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, popupPositionForRemote) popup.showInBestPositionFor(editor) } - if (sessionContext.perceivedLatency < 0) { - val perceivedLatency = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged() - CodeWhispererTelemetryService.getInstance().sendPerceivedLatencyEvent( - detailContexts[selectedIndex].requestId, - states.requestContext, - states.responseContext, - perceivedLatency - ) - CodeWhispererTelemetryService.getInstance().sendClientComponentLatencyEvent(states) - sessionContext.perceivedLatency = perceivedLatency - } } // popup.popupWindow is null in remote host @@ -399,7 +383,7 @@ class CodeWhispererPopupManager { } fun getReformattedRecommendation(detailContext: DetailContext, userInput: String) = - detailContext.reformatted.content().substring(userInput.length) + detailContext.completion.insertText.substring(userInput.length) fun initPopupListener(states: InvocationContext) { addPopupListener(states) @@ -568,31 +552,31 @@ class CodeWhispererPopupManager { popupComponents.nextButton.isEnabled = multipleRecommendation && validSelectedIndex != validCount - 1 } - private fun updateImportPanel(imports: List) { + private fun updateImportPanel(imports: List?) { popupComponents.panel.apply { if (components.contains(popupComponents.importPanel)) { remove(popupComponents.importPanel) } } - if (imports.isEmpty()) return + if (imports.isNullOrEmpty()) return val firstImport = imports.first() val choice = if (imports.size > 2) 2 else imports.size - 1 - val message = message("codewhisperer.popup.import_info", firstImport.statement(), imports.size - 1, choice) + val message = message("codewhisperer.popup.import_info", firstImport.statement, imports.size - 1, choice) popupComponents.panel.add(popupComponents.importPanel, horizontalPanelConstraints) popupComponents.importLabel.text = message } - private fun updateCodeReferencePanel(project: Project, references: List) { + private fun updateCodeReferencePanel(project: Project, references: List?) { popupComponents.panel.apply { if (components.contains(popupComponents.codeReferencePanel)) { remove(popupComponents.codeReferencePanel) } } - if (references.isEmpty()) return + if (references.isNullOrEmpty()) return popupComponents.panel.add(popupComponents.codeReferencePanel, horizontalPanelConstraints) - val licenses = references.map { it.licenseName() }.toSet() + val licenses = references.map { it.licenseName }.toSet() popupComponents.codeReferencePanelLink.apply { actionListeners.toList().forEach { removeActionListener(it) @@ -669,13 +653,13 @@ class CodeWhispererPopupManager { private fun isValidRecommendation(detailContext: DetailContext, userInput: String, typeahead: String): Boolean { if (detailContext.isDiscarded) return false - if (detailContext.recommendation.content().isEmpty()) return false + if (detailContext.completion.insertText.isEmpty()) return false val indexOfFirstNonWhiteSpace = typeahead.indexOfFirst { !it.isWhitespace() } if (indexOfFirstNonWhiteSpace == -1) return true for (i in 0..indexOfFirstNonWhiteSpace) { val subTypeahead = typeahead.substring(i) - if (detailContext.reformatted.content().startsWith(userInput + subTypeahead)) return true + if (detailContext.completion.insertText.startsWith(userInput + subTypeahead)) return true } return false } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt index 3bd7a19ffc0..1a32d30c045 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt @@ -38,15 +38,15 @@ import com.intellij.ui.popup.AbstractPopup import com.intellij.ui.popup.PopupFactoryImpl import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.UIUtil -import software.amazon.awssdk.services.codewhispererruntime.model.Import -import software.amazon.awssdk.services.codewhispererruntime.model.Reference import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionImports +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManagerNew import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.inlineLabelConstraints -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew +import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew @@ -203,8 +203,8 @@ class CodeWhispererPopupManagerNew { val validSelectedIndex = getValidSelectedIndex(selectedIndex) updateSelectedRecommendationLabelText(validSelectedIndex, validCount) updateNavigationPanel(validSelectedIndex, validCount) - updateImportPanel(previews[selectedIndex].detail.recommendation.mostRelevantMissingImports()) - updateCodeReferencePanel(sessionContext.project, previews[selectedIndex].detail.recommendation.references()) + updateImportPanel(previews[selectedIndex].detail.completion.mostRelevantMissingImports) + updateCodeReferencePanel(sessionContext.project, previews[selectedIndex].detail.completion.references) } fun render(sessionContext: SessionContextNew, isRecommendationAdded: Boolean) { @@ -220,7 +220,6 @@ class CodeWhispererPopupManagerNew { // 4. User navigating through the completions or typing as the completion shows. We should not update the latency // end time and should not emit any events in this case. if (!CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) { - sessionContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime() sessionContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime() val triggerTypeOfLastTrigger = CodeWhispererServiceNew.getInstance().getAllPaginationSessions() .values.filterNotNull().last().requestContext.triggerTypeInfo.triggerType @@ -253,8 +252,8 @@ class CodeWhispererPopupManagerNew { } val editor = sessionContext.editor val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo() - val userInputOriginal = previews[sessionContext.selectedIndex].userInput - val userInputLines = userInputOriginal.split("\n").size - 1 + val userInput = previews[sessionContext.selectedIndex].userInput + val userInputLines = userInput.split("\n").size - 1 val popupSize = (popup as AbstractPopup).preferredContentSize val yAboveFirstLine = p.y - popupSize.height + userInputLines * editor.lineHeight val popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height) @@ -325,8 +324,8 @@ class CodeWhispererPopupManagerNew { .setCancelOnWindowDeactivation(true) .createPopup() - fun getReformattedRecommendation(detailContext: DetailContextNew, userInput: String) = - detailContext.reformatted.content().substring(userInput.length) + fun getReformattedRecommendation(detailContext: DetailContext, userInput: String) = + detailContext.completion.insertText.substring(userInput.length) private fun initPopupListener(sessionContext: SessionContextNew, popup: JBPopup) { addPopupListener(popup) @@ -530,31 +529,31 @@ class CodeWhispererPopupManagerNew { popupComponents.nextButton.isEnabled = multipleRecommendation && validSelectedIndex != validCount - 1 } - private fun updateImportPanel(imports: List) { + private fun updateImportPanel(imports: List?) { popupComponents.panel.apply { if (components.contains(popupComponents.importPanel)) { remove(popupComponents.importPanel) } } - if (imports.isEmpty()) return + if (imports.isNullOrEmpty()) return val firstImport = imports.first() val choice = if (imports.size > 2) 2 else imports.size - 1 - val message = message("codewhisperer.popup.import_info", firstImport.statement(), imports.size - 1, choice) + val message = message("codewhisperer.popup.import_info", firstImport.statement, imports.size - 1, choice) popupComponents.panel.add(popupComponents.importPanel, horizontalPanelConstraints) popupComponents.importLabel.text = message } - private fun updateCodeReferencePanel(project: Project, references: List) { + private fun updateCodeReferencePanel(project: Project, references: List?) { popupComponents.panel.apply { if (components.contains(popupComponents.codeReferencePanel)) { remove(popupComponents.codeReferencePanel) } } - if (references.isEmpty()) return + if (references.isNullOrEmpty()) return popupComponents.panel.add(popupComponents.codeReferencePanel, horizontalPanelConstraints) - val licenses = references.map { it.licenseName() }.toSet() + val licenses = references.map { it.licenseName }.toSet() popupComponents.codeReferencePanelLink.apply { actionListeners.toList().forEach { removeActionListener(it) @@ -620,7 +619,7 @@ class CodeWhispererPopupManagerNew { private fun isValidRecommendation(preview: PreviewContext): Boolean { if (preview.detail.isDiscarded) return false - return preview.detail.recommendation.content().startsWith(preview.userInput + preview.typeahead) + return preview.detail.completion.insertText.startsWith(preview.userInput + preview.typeahead) } companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt index add8e835f02..dbf47f15240 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt @@ -27,10 +27,12 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { val document = editor.document val lineEndOffset = document.getLineEndOffset(document.getLineNumber(caretOffset)) + detail.hasSeen = true + // get matching brackets from recommendations to the brackets after caret position val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( detail, - states.recommendationContext.userInputSinceInvocation + states.recommendationContext.userInput ).substring(typeahead.length) val remainingLines = remaining.split("\n") @@ -41,7 +43,6 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { val matchingSymbols = editorManager.getMatchingSymbolsFromRecommendation( editor, firstLineOfRemaining, - detail.isTruncatedOnRight, sessionContext ) @@ -50,7 +51,7 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { } // Add the strike-though hint for the remaining non-matching first-line right context for multi-line completions - if (!detail.isTruncatedOnRight && otherLinesOfRemaining.isNotEmpty()) { + if (otherLinesOfRemaining.isNotEmpty()) { val rangeHighlighter = editor.markupModel.addRangeHighlighter( matchingSymbols[matchingSymbols.size - 2].second, lineEndOffset, @@ -76,7 +77,6 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { val overlappingLinesCount = editorManager.findOverLappingLines( editor, otherLinesOfRemaining, - detail.isTruncatedOnRight, sessionContext ) @@ -107,7 +107,7 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { // get matching brackets from recommendations to the brackets after caret position val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( detail, - states.recommendationContext.userInputSinceInvocation + states.recommendationContext.userInput ).substring(typeahead.length) val remainingLines = remaining.split("\n") @@ -117,7 +117,6 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { val overlappingLinesCount = editorManager.findOverLappingLines( editor, otherLinesOfRemaining, - detail.isTruncatedOnRight, sessionContext ) CodeWhispererPopupManager.getInstance().render( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt index ac7ccbe21a7..d53238f929f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt @@ -45,7 +45,6 @@ class CodeWhispererUIChangeListenerNew : CodeWhispererPopupStateChangeListener { val matchingSymbols = editorManager.getMatchingSymbolsFromRecommendation( editor, firstLineOfRemaining, - detail.isTruncatedOnRight, sessionContext ) @@ -54,7 +53,7 @@ class CodeWhispererUIChangeListenerNew : CodeWhispererPopupStateChangeListener { } // Add the strike-though hint for the remaining non-matching first-line right context for multi-line completions - if (!detail.isTruncatedOnRight && otherLinesOfRemaining.isNotEmpty()) { + if (otherLinesOfRemaining.isNotEmpty()) { val rangeHighlighter = editor.markupModel.addRangeHighlighter( matchingSymbols[matchingSymbols.size - 2].second, lineEndOffset, @@ -80,7 +79,6 @@ class CodeWhispererUIChangeListenerNew : CodeWhispererPopupStateChangeListener { val overlappingLinesCount = editorManager.findOverLappingLines( editor, otherLinesOfRemaining, - detail.isTruncatedOnRight, sessionContext ) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt index 10f6438e882..39644c1fd4f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt @@ -8,8 +8,6 @@ import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.util.SystemInfo import com.intellij.util.Alarm import com.intellij.util.AlarmFactory import kotlinx.coroutines.Job @@ -18,20 +16,11 @@ import org.apache.commons.collections4.queue.CircularFifoQueue import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.core.coroutines.applicationCoroutineScope import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew -import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState import software.aws.toolkits.telemetry.CodewhispererTriggerType import java.time.Duration import java.time.Instant -import kotlin.math.exp - -data class ClassifierResult(val shouldTrigger: Boolean, val calculatedResult: Double = 0.0) @Service class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposable { @@ -47,38 +36,9 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa scheduleReset() } - fun addPreviousDecision(decision: CodewhispererPreviousSuggestionState) { - previousUserTriggerDecisions.add(decision) - } - - // a util wrapper - fun tryInvokeAutoTrigger(editor: Editor, triggerType: CodeWhispererAutomatedTriggerType): Job? { - // only needed for Classifier group, thus calculate it lazily - timeAtLastCharTyped = System.nanoTime() - val classifierResult: ClassifierResult by lazy { shouldTriggerClassifier(editor, triggerType.telemetryType) } - - // we need classifier result for any type of triggering for classifier group for supported languages - triggerType.calculationResult = classifierResult.calculatedResult - - return when (triggerType) { - // only invoke service if result > threshold for classifier trigger - is CodeWhispererAutomatedTriggerType.Classifier -> run { - if (classifierResult.shouldTrigger) { - invoke(editor, triggerType) - } else { - null - } - } - - // invoke whatever the result is for char / enter based trigger - else -> run { - invoke(editor, triggerType) - } - } - } - // real auto trigger logic fun invoke(editor: Editor, triggerType: CodeWhispererAutomatedTriggerType): Job? { + timeAtLastCharTyped = System.nanoTime() if (!( if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) { CodeWhispererServiceNew.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger) @@ -94,7 +54,6 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa lastInvocationLineNum = runReadAction { editor.caretModel.visualPosition.line } val latencyContext = LatencyContext().apply { - codewhispererPreprocessingStart = System.nanoTime() codewhispererEndToEndStart = System.nanoTime() } @@ -121,203 +80,9 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa } } - fun shouldTriggerClassifier( - editor: Editor, - automatedTriggerType: CodewhispererAutomatedTriggerType = CodewhispererAutomatedTriggerType.Classifier, // TODO: need this? - ): ClassifierResult { - val caretContext = runReadAction { CodeWhispererEditorUtil.extractCaretContext(editor) } - val language = runReadAction { - FileDocumentManager.getInstance().getFile(editor.document)?.programmingLanguage() - } ?: CodeWhispererUnknownLanguage.INSTANCE - val caretPosition = runReadAction { CodeWhispererEditorUtil.getCaretPosition(editor) } - - val leftContextLines = caretContext.leftFileContext.split(Regex("\r?\n")) - val leftContextLength = caretContext.leftFileContext.length - val leftContextAtCurrentLine = if (leftContextLines.size - 1 >= 0) leftContextLines[leftContextLines.size - 1] else "" - var keyword = "" - val lastToken = leftContextAtCurrentLine.trim().split(" ").let { tokens -> - if (tokens.size - 1 >= 0) tokens[tokens.size - 1] else "" - } - if (lastToken.length > 1) keyword = lastToken - - val lengthOfLeftCurrent = leftContextAtCurrentLine.length - val lengthOfLeftPrev = if (leftContextLines.size - 2 >= 0) { - leftContextLines[leftContextLines.size - 2].length.toDouble() - } else { - 0.0 - } - - val rightContext = caretContext.rightFileContext - val lengthOfRight = rightContext.trim().length - - val triggerTypeCoefficient = CodeWhispererClassifierConstants.triggerTypeCoefficientMap[automatedTriggerType] ?: 0.0 - - val osCoefficient: Double = if (SystemInfo.isMac) { - CodeWhispererClassifierConstants.osMap["Mac OS X"] ?: 0.0 - } else if (SystemInfo.isWindows) { - val osVersion = SystemInfo.OS_VERSION - if (osVersion.contains("11", true) || osVersion.contains("10", true)) { - CodeWhispererClassifierConstants.osMap["Windows 10"] - } else { - CodeWhispererClassifierConstants.osMap["Windows"] - } - } else { - 0.0 - } ?: 0.0 - - val lastCharCoefficient = if (leftContextAtCurrentLine.length - 1 >= 0) { - CodeWhispererClassifierConstants.coefficientsMap[leftContextAtCurrentLine[leftContextAtCurrentLine.length - 1].toString()] ?: 0.0 - } else { - 0.0 - } - - val keywordCoefficient = CodeWhispererClassifierConstants.coefficientsMap[keyword] ?: 0.0 - val averageLanguageCoefficient = CodeWhispererClassifierConstants.languageMap.values.average() - val languageCoefficient = CodeWhispererClassifierConstants.languageMap[language] ?: averageLanguageCoefficient - val ideCoefficient = 0.0 - - var previousOneAccept: Double = 0.0 - var previousOneReject: Double = 0.0 - var previousOneOther: Double = 0.0 - val previousOneDecision = - if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) { - CodeWhispererTelemetryServiceNew.getInstance().previousUserTriggerDecision - } else { - CodeWhispererTelemetryService.getInstance().previousUserTriggerDecision - } - if (previousOneDecision == null) { - previousOneAccept = 0.0 - previousOneReject = 0.0 - previousOneOther = 0.0 - } else { - previousOneAccept = - if (previousOneDecision == CodewhispererPreviousSuggestionState.Accept) { - CodeWhispererClassifierConstants.prevDecisionAcceptCoefficient - } else { - 0.0 - } - previousOneReject = - if (previousOneDecision == CodewhispererPreviousSuggestionState.Reject) { - CodeWhispererClassifierConstants.prevDecisionRejectCoefficient - } else { - 0.0 - } - previousOneOther = - if ( - previousOneDecision != CodewhispererPreviousSuggestionState.Accept && - previousOneDecision != CodewhispererPreviousSuggestionState.Reject - ) { - CodeWhispererClassifierConstants.prevDecisionOtherCoefficient - } else { - 0.0 - } - } - - var leftContextLengthCoefficient: Double = 0.0 - - leftContextLengthCoefficient = when (leftContextLength) { - in 0..4 -> CodeWhispererClassifierConstants.lengthLeft0To5 - in 5..9 -> CodeWhispererClassifierConstants.lengthLeft5To10 - in 10..19 -> CodeWhispererClassifierConstants.lengthLeft10To20 - in 20..29 -> CodeWhispererClassifierConstants.lengthLeft20To30 - in 30..39 -> CodeWhispererClassifierConstants.lengthLeft30To40 - in 40..49 -> CodeWhispererClassifierConstants.lengthLeft40To50 - else -> 0.0 - } - - val normalizedLengthOfRight = CodeWhispererClassifierConstants.lengthofRightCoefficient * VariableTypeNeedNormalize.LenRight.normalize( - lengthOfRight.toDouble() - ) - - val normalizedLengthOfLeftCurrent = CodeWhispererClassifierConstants.lengthOfLeftCurrentCoefficient * VariableTypeNeedNormalize.LenLeftCur.normalize( - lengthOfLeftCurrent.toDouble() - ) - - val normalizedLengthOfPrev = CodeWhispererClassifierConstants.lengthOfLeftPrevCoefficient * VariableTypeNeedNormalize.LenLeftPrev.normalize( - lengthOfLeftPrev - ) - - val normalizedLineNum = CodeWhispererClassifierConstants.lineNumCoefficient * VariableTypeNeedNormalize.LineNum.normalize(caretPosition.line.toDouble()) - - val intercept = CodeWhispererClassifierConstants.intercept - - val resultBeforeSigmoid = - normalizedLengthOfRight + - normalizedLengthOfLeftCurrent + - normalizedLengthOfPrev + - normalizedLineNum + - languageCoefficient + - osCoefficient + - triggerTypeCoefficient + - lastCharCoefficient + - keywordCoefficient + - ideCoefficient + - previousOneAccept + - previousOneReject + - previousOneOther + - leftContextLengthCoefficient + - intercept - - val shouldTrigger = sigmoid(resultBeforeSigmoid) > getThreshold() - - return ClassifierResult(shouldTrigger, sigmoid(resultBeforeSigmoid)) - } - override fun dispose() {} companion object { - private const val triggerThreshold: Double = 0.43 - fun getInstance(): CodeWhispererAutoTriggerService = service() - - fun getThreshold(): Double = triggerThreshold - - fun sigmoid(x: Double): Double = 1 / (1 + exp(-x)) - } -} - -private enum class VariableTypeNeedNormalize { - Cursor { - override fun normalize(value: Double): Double = 0.0 - }, - LineNum { - override fun normalize(value: Double): Double = (value - minn.lineNum) / (maxx.lineNum - minn.lineNum) - }, - LenLeftCur { - override fun normalize(value: Double): Double = (value - minn.lenLeftCur) / (maxx.lenLeftCur - minn.lenLeftCur) - }, - LenLeftPrev { - override fun normalize(value: Double): Double = (value - minn.lenLeftPrev) / (maxx.lenLeftPrev - minn.lenLeftPrev) - }, - LenRight { - override fun normalize(value: Double): Double = (value - minn.lenRight) / (maxx.lenRight - minn.lenRight) - }, - LineDiff { - override fun normalize(value: Double): Double = 0.0 - }, ; - - abstract fun normalize(toDouble: Double): Double - - data class NormalizedCoefficients( - val lineNum: Double, - val lenLeftCur: Double, - val lenLeftPrev: Double, - val lenRight: Double, - ) - - companion object { - private val maxx = NormalizedCoefficients( - lineNum = 4631.0, - lenLeftCur = 157.0, - lenLeftPrev = 176.0, - lenRight = 10239.0, - ) - - private val minn = NormalizedCoefficients( - lineNum = 0.0, - lenLeftCur = 0.0, - lenLeftPrev = 0.0, - lenRight = 0.0, - ) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutomatedTriggerType.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutomatedTriggerType.kt index ff0c25fe2cd..fbb006ca402 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutomatedTriggerType.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutomatedTriggerType.kt @@ -7,7 +7,6 @@ import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType sealed class CodeWhispererAutomatedTriggerType( val telemetryType: CodewhispererAutomatedTriggerType, - var calculationResult: Double? = null, ) { class Classifier : CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.Classifier) class SpecialChar(val specialChar: Char) : diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt index c77381979f3..c58c4b4a466 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.components.service import com.intellij.util.messages.Topic import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import java.time.Duration import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean @@ -23,7 +22,7 @@ class CodeWhispererInvocationStatus { private set private var isDisplaySessionActive: Boolean = false private var timeAtLastInvocationStart: Instant? = null - var popupStartTimestamp: Instant? = null + var completionShownTime: Instant? = null private set fun checkExistingInvocationAndSet(): Boolean = @@ -54,8 +53,8 @@ class CodeWhispererInvocationStatus { timeAtLastDocumentChanged = Instant.now() } - fun setPopupStartTimestamp() { - popupStartTimestamp = Instant.now() + fun completionShown() { + completionShownTime = Instant.now() } fun getTimeSinceDocumentChanged(): Double { @@ -64,11 +63,6 @@ class CodeWhispererInvocationStatus { return timeInDouble } - fun hasEnoughDelayToShowCodeWhisperer(): Boolean { - val timeCanShowCodeWhisperer = timeAtLastDocumentChanged.plusMillis(CodeWhispererConstants.POPUP_DELAY) - return timeCanShowCodeWhisperer.isBefore(Instant.now()) - } - fun isDisplaySessionActive(): Boolean = isDisplaySessionActive fun setDisplaySessionActive(value: Boolean) { @@ -84,11 +78,6 @@ class CodeWhispererInvocationStatus { invokingSessionId = sessionId } - fun hasEnoughDelayToInvokeCodeWhisperer(): Boolean { - val timeCanShowCodeWhisperer = timeAtLastInvocationStart?.plusMillis(CodeWhispererConstants.INVOCATION_INTERVAL) ?: return true - return timeCanShowCodeWhisperer.isBefore(Instant.now()) - } - companion object { private val LOG = getLogger() fun getInstance(): CodeWhispererInvocationStatus = service() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererRecommendationManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererRecommendationManager.kt index 486f2e2eba9..b04d85700d5 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererRecommendationManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererRecommendationManager.kt @@ -5,82 +5,13 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import org.jetbrains.annotations.VisibleForTesting -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.Span +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType -import kotlin.math.max -import kotlin.math.min @Service class CodeWhispererRecommendationManager { - fun reformatReference(requestContext: RequestContext, recommendation: Completion): Completion { - // startOffset is the offset at the start of user input since invocation - val invocationStartOffset = requestContext.caretPosition.offset - - val startOffsetSinceUserInput = requestContext.editor.caretModel.offset - val endOffset = invocationStartOffset + recommendation.content().length - - if (startOffsetSinceUserInput > endOffset) return recommendation - - val reformattedReferences = recommendation.references().filter { - val referenceStart = invocationStartOffset + it.recommendationContentSpan().start() - val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end() - referenceStart < endOffset && referenceEnd > startOffsetSinceUserInput - }.map { - val referenceStart = invocationStartOffset + it.recommendationContentSpan().start() - val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end() - val updatedReferenceStart = max(referenceStart, startOffsetSinceUserInput) - val updatedReferenceEnd = min(referenceEnd, endOffset) - it.toBuilder().recommendationContentSpan( - Span.builder() - .start(updatedReferenceStart - invocationStartOffset) - .end(updatedReferenceEnd - invocationStartOffset) - .build() - ).build() - } - - return Completion.builder() - .content(recommendation.content()) - .references(reformattedReferences) - .build() - } - - fun reformatReference(requestContext: RequestContextNew, recommendation: Completion): Completion { - // startOffset is the offset at the start of user input since invocation - val invocationStartOffset = requestContext.caretPosition.offset - - val startOffsetSinceUserInput = requestContext.editor.caretModel.offset - val endOffset = invocationStartOffset + recommendation.content().length - - if (startOffsetSinceUserInput > endOffset) return recommendation - - val reformattedReferences = recommendation.references().filter { - val referenceStart = invocationStartOffset + it.recommendationContentSpan().start() - val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end() - referenceStart < endOffset && referenceEnd > startOffsetSinceUserInput - }.map { - val referenceStart = invocationStartOffset + it.recommendationContentSpan().start() - val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end() - val updatedReferenceStart = max(referenceStart, startOffsetSinceUserInput) - val updatedReferenceEnd = min(referenceEnd, endOffset) - it.toBuilder().recommendationContentSpan( - Span.builder() - .start(updatedReferenceStart - invocationStartOffset) - .end(updatedReferenceEnd - invocationStartOffset) - .build() - ).build() - } - - return Completion.builder() - .content(recommendation.content()) - .references(reformattedReferences) - .build() - } - fun buildRecommendationChunks( recommendation: String, matchingSymbols: List>, @@ -92,230 +23,19 @@ class CodeWhispererRecommendationManager { } fun buildDetailContext( - requestContext: RequestContext, userInput: String, - recommendations: List, - requestId: String, - ): List { - val seen = mutableSetOf() - return recommendations.map { - val isDiscardedByUserInput = !it.content().startsWith(userInput) || it.content() == userInput - if (isDiscardedByUserInput) { - return@map DetailContext( - requestId, - it, - it, - isDiscarded = true, - isTruncatedOnRight = false, - rightOverlap = "", - getCompletionType(it) - ) - } - - val overlap = findRightContextOverlap(requestContext, it) - val overlapIndex = it.content().lastIndexOf(overlap) - val truncatedContent = - if (overlap.isNotEmpty() && overlapIndex >= 0) { - it.content().substring(0, overlapIndex) - } else { - it.content() - } - val truncated = it.toBuilder() - .content(truncatedContent) - .build() - val isDiscardedByUserInputForTruncated = !truncated.content().startsWith(userInput) || truncated.content() == userInput - if (isDiscardedByUserInputForTruncated) { - return@map DetailContext( - requestId, - it, - truncated, - isDiscarded = true, - isTruncatedOnRight = true, - rightOverlap = overlap, - getCompletionType(it) - ) - } - - val isDiscardedByRightContextTruncationDedupe = !seen.add(truncated.content()) - val isDiscardedByBlankAfterTruncation = truncated.content().isBlank() - if (isDiscardedByRightContextTruncationDedupe || isDiscardedByBlankAfterTruncation) { - return@map DetailContext( - requestId, - it, - truncated, - isDiscarded = true, - truncated.content().length != it.content().length, - overlap, - getCompletionType(it) - ) - } - val reformatted = reformatReference(requestContext, truncated) + completions: InlineCompletionListWithReferences, + ): MutableList = + completions.items.map { DetailContext( - requestId, + it.itemId, it, - reformatted, - isDiscarded = false, - truncated.content().length != it.content().length, - overlap, + isDiscarded = !it.insertText.startsWith(userInput) || it.insertText == userInput, getCompletionType(it) ) }.toMutableList() - } - - fun buildDetailContext( - requestContext: RequestContextNew, - userInput: String, - recommendations: List, - requestId: String, - ): MutableList { - val seen = mutableSetOf() - return recommendations.map { - val isDiscardedByUserInput = !it.content().startsWith(userInput) || it.content() == userInput - if (isDiscardedByUserInput) { - return@map DetailContextNew( - requestId, - it, - it, - isDiscarded = true, - isTruncatedOnRight = false, - rightOverlap = "", - getCompletionType(it) - ) - } - - val overlap = findRightContextOverlap(requestContext, it) - val overlapIndex = it.content().lastIndexOf(overlap) - val truncatedContent = - if (overlap.isNotEmpty() && overlapIndex >= 0) { - it.content().substring(0, overlapIndex) - } else { - it.content() - } - val truncated = it.toBuilder() - .content(truncatedContent) - .build() - val isDiscardedByUserInputForTruncated = !truncated.content().startsWith(userInput) || truncated.content() == userInput - if (isDiscardedByUserInputForTruncated) { - return@map DetailContextNew( - requestId, - it, - truncated, - isDiscarded = true, - isTruncatedOnRight = true, - rightOverlap = overlap, - getCompletionType(it) - ) - } - - val isDiscardedByRightContextTruncationDedupe = !seen.add(truncated.content()) - val isDiscardedByBlankAfterTruncation = truncated.content().isBlank() - if (isDiscardedByRightContextTruncationDedupe || isDiscardedByBlankAfterTruncation) { - return@map DetailContextNew( - requestId, - it, - truncated, - isDiscarded = true, - truncated.content().length != it.content().length, - overlap, - getCompletionType(it) - ) - } - val reformatted = reformatReference(requestContext, truncated) - DetailContextNew( - requestId, - it, - reformatted, - isDiscarded = false, - truncated.content().length != it.content().length, - overlap, - getCompletionType(it) - ) - }.toMutableList() - } - - fun findRightContextOverlap( - requestContext: RequestContext, - recommendation: Completion, - ): String { - val document = requestContext.editor.document - val caret = requestContext.editor.caretModel.primaryCaret - val rightContext = document.charsSequence.subSequence(caret.offset, document.charsSequence.length).toString() - val recommendationContent = recommendation.content() - return findRightContextOverlap(rightContext, recommendationContent) - } - - fun findRightContextOverlap( - requestContext: RequestContextNew, - recommendation: Completion, - ): String { - val document = requestContext.editor.document - val caret = requestContext.editor.caretModel.primaryCaret - val rightContext = document.charsSequence.subSequence(caret.offset, document.charsSequence.length).toString() - val recommendationContent = recommendation.content() - return findRightContextOverlap(rightContext, recommendationContent) - } - - @VisibleForTesting - fun findRightContextOverlap(rightContext: String, recommendationContent: String): String { - val rightContextFirstLine = rightContext.substringBefore("\n") - val overlap = - if (rightContextFirstLine.isEmpty()) { - val tempOverlap = overlap(recommendationContent, rightContext) - if (tempOverlap.isEmpty()) overlap(recommendationContent.trimEnd(), trimExtraPrefixNewLine(rightContext)) else tempOverlap - } else { - // this is necessary to prevent display issue if first line of right context is not empty - var tempOverlap = overlap(recommendationContent, rightContext) - if (tempOverlap.isEmpty()) { - tempOverlap = overlap(recommendationContent.trimEnd(), trimExtraPrefixNewLine(rightContext)) - } - if (recommendationContent.substring(0, recommendationContent.length - tempOverlap.length).none { it == '\n' }) { - tempOverlap - } else { - "" - } - } - return overlap - } - - fun overlap(first: String, second: String): String { - for (i in max(0, first.length - second.length) until first.length) { - val suffix = first.substring(i) - if (second.startsWith(suffix)) { - return suffix - } - } - return "" - } companion object { fun getInstance(): CodeWhispererRecommendationManager = service() - - /** - * a function to trim extra prefixing new line character (only leave 1 new line character) - * example: - * content = "\n\n\nfoo\n\nbar\nbaz" - * return = "\nfoo\n\nbar\nbaz" - * - * example: - * content = "\n\n\tfoobar\nbaz" - * return = "\n\tfoobar\nbaz" - */ - fun trimExtraPrefixNewLine(content: String): String { - if (content.isEmpty()) { - return "" - } - - val firstChar = content.first() - if (firstChar != '\n') { - return content - } - - var index = 1 - while (index < content.length && content[index] == '\n') { - index++ - } - - return firstChar + content.substring(index) - } } } 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 67ff8112772..6dde06c20a3 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 @@ -4,10 +4,9 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service import com.intellij.codeInsight.hint.HintManager -import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service @@ -20,82 +19,59 @@ import com.intellij.openapi.util.Disposer import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.messages.Topic import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.JsonRpcException +import org.eclipse.lsp4j.jsonrpc.messages.Either import software.amazon.awssdk.core.exception.SdkServiceException -import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.FileContext -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.amazon.awssdk.services.codewhispererruntime.model.IdeDiagnostic -import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage -import software.amazon.awssdk.services.codewhispererruntime.model.RecommendationsWithReferencesPreference -import software.amazon.awssdk.services.codewhispererruntime.model.ResourceNotFoundException -import software.amazon.awssdk.services.codewhispererruntime.model.SupplementalContext import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException 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.coroutines.EDT -import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext -import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope 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.amazonq.lsp.model.aws.textDocument.InlineCompletionContext +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionTriggerKind +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionWithReferencesParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil.toUriString import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.getCaretPosition -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.isSupportedJsonFormat import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJson import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.WorkerContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeInsightsSettingsFacade import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.isInjectedText import software.aws.toolkits.jetbrains.utils.isQExpired -import software.aws.toolkits.jetbrains.utils.notifyWarn 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 @@ -135,8 +111,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val project = editor.project ?: return if (!isCodeWhispererEnabled(project)) return - latencyContext.credentialFetchingStart = System.nanoTime() - // try to refresh automatically if possible, otherwise ask user to login again if (isQExpired(project)) { // consider changing to only running once a ~minute since this is relatively expensive @@ -160,7 +134,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } } - latencyContext.credentialFetchingEnd = System.nanoTime() val psiFile = runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(editor.document) } if (psiFile == null) { @@ -177,28 +150,10 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { getRequestContext(triggerTypeInfo, editor, project, psiFile, latencyContext) } catch (e: Exception) { LOG.debug { e.message.toString() } - CodeWhispererTelemetryService.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) return } - val language = requestContext.fileContextInfo.programmingLanguage - val leftContext = requestContext.fileContextInfo.caretContext.leftFileContext - if (!language.isCodeCompletionSupported() || ( - language is CodeWhispererJson && !isSupportedJsonFormat( - requestContext.fileContextInfo.filename, - leftContext - ) - ) - ) { - LOG.debug { "Programming language $language is not supported by CodeWhisperer" } - if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { - showCodeWhispererInfoHint( - requestContext.editor, - message("codewhisperer.language.error", psiFile.fileType.name) - ) - } - return - } + // TODO flare: since IDE local language check got removed, flare needs to implement json aws template support only LOG.debug { "Calling CodeWhisperer service, trigger type: ${triggerTypeInfo.triggerType}" + @@ -224,176 +179,47 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } } - val workerContexts = mutableListOf() - // When popup is disposed we will cancel this coroutine. The only places popup can get disposed should be - // from CodeWhispererPopupManager.cancelPopup() and CodeWhispererPopupManager.closePopup(). - // It's possible and ok that coroutine will keep running until the next time we check it's state. - // As long as we don't show to the user extra info we are good. - val coroutineScope = disposableCoroutineScope(popup) - var states: InvocationContext? = null - var lastRecommendationIndex = -1 - val job = coroutineScope.launch { + val job = cs.launch { try { - val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( - buildCodeWhispererRequest( - requestContext.fileContextInfo, - requestContext.awaitSupplementalContext(), - requestContext.customizationArn, - requestContext.profileArn, - requestContext.workspaceId, - ) - ) - var startTime = System.nanoTime() - requestContext.latencyContext.codewhispererPreprocessingEnd = System.nanoTime() - requestContext.latencyContext.paginationAllCompletionsStart = System.nanoTime() CodeWhispererInvocationStatus.getInstance().setInvocationStart() - var requestCount = 0 - for (response in responseIterable) { - requestCount++ + var nextToken: Either? = null + do { + val result = AmazonQLspService.executeIfRunning(requestContext.project) { server -> + val params = createInlineCompletionParams(requestContext.editor, requestContext.triggerTypeInfo, nextToken) + server.inlineCompletionWithReferences(params) + } + val completion = result?.await() ?: break + nextToken = completion.partialResultToken val endTime = System.nanoTime() val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble() startTime = endTime - val requestId = response.responseMetadata().requestId() - val sessionId = response.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] - if (requestCount == 1) { - requestContext.latencyContext.codewhispererPostprocessingStart = System.nanoTime() - requestContext.latencyContext.paginationFirstCompletionTime = - (endTime - requestContext.latencyContext.codewhispererEndToEndStart).toDouble() - requestContext.latencyContext.firstRequestId = requestId - CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId) - } - if (response.nextToken().isEmpty()) { - requestContext.latencyContext.paginationAllCompletionsEnd = System.nanoTime() - } - val responseContext = ResponseContext(sessionId) - logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null) - lastRecommendationIndex += response.completions().size - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) - .onSuccess(requestContext.fileContextInfo) - broadcastQEvent(QFeatureEvent.INVOCATION) - CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - true, - latency, - null - ) - - val validatedResponse = validateResponse(response) + val responseContext = ResponseContext(completion.sessionId) + logServiceInvocation(requestContext, responseContext, completion, latency, null) + val workerContext = WorkerContext(requestContext, responseContext, completion, popup) runInEdt { - // If delay is not met, add them to the worker queue and process them later. - // On first response, workers queue must be empty. If there's enough delay before showing, - // process CodeWhisperer UI rendering and workers queue will remain empty throughout this - // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task - // will be added to the workers queue. - // On subsequent responses, if they see workers queue is not empty, it means the first worker - // task hasn't been finished yet, in this case simply add another task to the queue. If they - // see worker queue is empty, the previous tasks must have been finished before this. In this - // case render CodeWhisperer UI directly. - val workerContext = WorkerContext(requestContext, responseContext, validatedResponse, popup) - if (workerContexts.isNotEmpty()) { - workerContexts.add(workerContext) - } else { - if (states == null && !popup.isDisposed && - !CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer() - ) { - // It's the first response, and no enough delay before showing - projectCoroutineScope(requestContext.project).launch { - while (!CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer()) { - delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL) - } - runInEdt { - workerContexts.forEach { - states = processCodeWhispererUI(it, states) - } - workerContexts.clear() - } - } - workerContexts.add(workerContext) - } else { - // Have enough delay before showing for the first response, or it's subsequent responses - states = processCodeWhispererUI(workerContext, states) - } - } + states = processCodeWhispererUI(workerContext, states) } - if (!isActive) { - // If job is cancelled before we do another request, don't bother making - // another API call to save resources + if (popup.isDisposed) { LOG.debug { "Skipping sending remaining requests on CodeWhisperer session exit" } - break + return@launch } - } + } while (nextToken != null && nextToken.left.isNotEmpty()) } catch (e: Exception) { - val requestId: String - val sessionId: String + // TODO flare: flare doesn't return exceptions + val sessionId = "" val displayMessage: String - if ( - CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) || - e is ResourceNotFoundException - ) { - (e as CodeWhispererRuntimeException) - - requestId = e.requestId() ?: "" - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] - val exceptionType = e::class.simpleName - val responseContext = ResponseContext(sessionId) - - CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - false, - 0.0, - exceptionType - ) - - LOG.debug { - "The provided customization ${requestContext.customizationArn} is not found, " + - "will fallback to the default and retry generate completion" - } - logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType) - - notifyWarn( - title = "", - content = message("codewhisperer.notification.custom.not_available"), - project = requestContext.project, - notificationActions = listOf( - NotificationAction.create( - message("codewhisperer.notification.custom.simple.button.select_another_customization") - ) { _, notification -> - CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project) - notification.expire() - } - ) - ) - CodeWhispererInvocationStatus.getInstance().finishInvocation() - CodeWhispererInvocationStatus.getInstance().setInvocationComplete() - - requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) } - - projectCoroutineScope(requestContext.project).launch { - showRecommendationsInPopup( - requestContext.editor, - requestContext.triggerTypeInfo, - requestContext.latencyContext - ) + if (e is JsonRpcException) { + // TODO: only log once to avoid auto-trigger spam? + LOG.debug(e) { + "Error talking to Q LSP server" } - return@launch - } else if (e is CodeWhispererRuntimeException) { - requestId = e.requestId() ?: "" - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] - displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side") + displayMessage = "Q LSP server failed to communicate, try restarting the current project." } else { - requestId = "" - sessionId = "" val statusCode = if (e is SdkServiceException) e.statusCode() else 0 displayMessage = if (statusCode >= 500) { @@ -408,16 +234,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val exceptionType = e::class.simpleName val responseContext = ResponseContext(sessionId) CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId) - logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType) - CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - false, - 0.0, - exceptionType - ) + logServiceInvocation(requestContext, responseContext, null, null, exceptionType) if (e is ThrottlingException && e.message == CodeWhispererConstants.THROTTLING_MESSAGE @@ -456,26 +273,26 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { private fun processCodeWhispererUI(workerContext: WorkerContext, currStates: InvocationContext?): InvocationContext? { val requestContext = workerContext.requestContext val responseContext = workerContext.responseContext - val response = workerContext.response + val completions = workerContext.completions val popup = workerContext.popup - val requestId = response.responseMetadata().requestId() // At this point when we are in EDT, the state of the popup will be thread-safe // across this thread execution, so if popup is disposed, we will stop here. // This extra check is needed because there's a time between when we get the response and // when we enter the EDT. if (popup.isDisposed) { - LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. RequestId: $requestId" } + LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. session id: ${responseContext.sessionId}" } return null } if (requestContext.editor.isDisposed) { - LOG.debug { "Stop showing CodeWhisperer recommendations since editor is disposed. RequestId: $requestId" } + LOG.debug { "Stop showing CodeWhisperer recommendations since editor is disposed. session id: ${responseContext.sessionId}" } + sendDiscardedUserDecisionEventForAll(requestContext.project, requestContext.latencyContext, responseContext.sessionId, completions) CodeWhispererPopupManager.getInstance().cancelPopup(popup) return null } - if (response.nextToken().isEmpty()) { + if (completions.partialResultToken?.left.isNullOrEmpty()) { CodeWhispererInvocationStatus.getInstance().finishInvocation() } @@ -487,47 +304,29 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val nextStates: InvocationContext? if (currStates == null) { // first response - nextStates = initStates(requestContext, responseContext, response, caretMovement, popup) + nextStates = initStates(requestContext, responseContext, completions, caretMovement, popup) isPopupShowing = false // receiving a null state means caret has moved backward or there's a conflict with // Intellisense popup, so we are going to cancel the job if (nextStates == null) { - LOG.debug { "Cancelling popup and exiting CodeWhisperer session. RequestId: $requestId" } + LOG.debug { "Cancelling popup and exiting CodeWhisperer session. session id: ${responseContext.sessionId}" } + sendDiscardedUserDecisionEventForAll(requestContext.project, requestContext.latencyContext, responseContext.sessionId, completions) CodeWhispererPopupManager.getInstance().cancelPopup(popup) return null } } else { // subsequent responses - nextStates = updateStates(currStates, response) + nextStates = updateStates(currStates, completions) isPopupShowing = checkRecommendationsValidity(currStates, false) } - val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, response.nextToken().isEmpty()) + val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, completions.partialResultToken == null) // If there are no recommendations at all in this session, we need to manually send the user decision event here // since it won't be sent automatically later - if (nextStates.recommendationContext.details.isEmpty() && response.nextToken().isEmpty()) { - LOG.debug { "Received just an empty list from this session, requestId: $requestId" } - CodeWhispererTelemetryService.getInstance().sendUserDecisionEvent( - requestContext, - responseContext, - DetailContext( - requestId, - Completion.builder().build(), - Completion.builder().build(), - false, - false, - "", - CodewhispererCompletionType.Line - ), - -1, - CodewhispererSuggestionState.Empty, - nextStates.recommendationContext.details.size - ) - } if (!hasAtLeastOneValid) { - if (response.nextToken().isEmpty()) { + if (completions.partialResultToken?.left.isNullOrEmpty()) { LOG.debug { "None of the recommendations are valid, exiting CodeWhisperer session" } CodeWhispererPopupManager.getInstance().cancelPopup(popup) return null @@ -541,63 +340,49 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { private fun initStates( requestContext: RequestContext, responseContext: ResponseContext, - response: GenerateCompletionsResponse, + completions: InlineCompletionListWithReferences, caretMovement: CaretMovement, popup: JBPopup, ): InvocationContext? { - val requestId = response.responseMetadata().requestId() - val recommendations = response.completions() val visualPosition = requestContext.editor.caretModel.visualPosition if (CodeWhispererPopupManager.getInstance().hasConflictingPopups(requestContext.editor)) { LOG.debug { "Detect conflicting popup window with CodeWhisperer popup, not showing CodeWhisperer popup" } - sendDiscardedUserDecisionEventForAll(requestContext, responseContext, recommendations) return null } if (caretMovement == CaretMovement.MOVE_BACKWARD) { - LOG.debug { "Caret moved backward, discarding all of the recommendations. Request ID: $requestId" } - sendDiscardedUserDecisionEventForAll(requestContext, responseContext, recommendations) + LOG.debug { "Caret moved backward, discarding all of the recommendations. Session Id: ${completions.sessionId}" } return null } - val userInputOriginal = CodeWhispererEditorManager.getInstance().getUserInputSinceInvocation( - requestContext.editor, - requestContext.caretPosition.offset - ) val userInput = if (caretMovement == CaretMovement.NO_CHANGE) { - LOG.debug { "Caret position not changed since invocation. Request ID: $requestId" } + LOG.debug { "Caret position not changed since invocation. Session Id: ${completions.sessionId}" } "" } else { - userInputOriginal.trimStart().also { - LOG.debug { - "Caret position moved forward since invocation. Request ID: $requestId, " + - "user input since invocation: $userInputOriginal, " + - "user input without leading spaces: $it" - } - } + LOG.debug { "Caret position moved forward since invocation. Session Id: ${completions.sessionId}" } + CodeWhispererEditorManager.getInstance().getUserInputSinceInvocation( + requestContext.editor, + requestContext.caretPosition.offset + ) } val detailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - requestContext, userInput, - recommendations, - requestId + completions, ) - val recommendationContext = RecommendationContext(detailContexts, userInputOriginal, userInput, visualPosition) + val recommendationContext = RecommendationContext(detailContexts, userInput, visualPosition) return buildInvocationContext(requestContext, responseContext, recommendationContext, popup) } private fun updateStates( states: InvocationContext, - response: GenerateCompletionsResponse, + completions: InlineCompletionListWithReferences, ): InvocationContext { val recommendationContext = states.recommendationContext val details = recommendationContext.details val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - states.requestContext, - recommendationContext.userInputSinceInvocation, - response.completions(), - response.responseMetadata().requestId() + recommendationContext.userInput, + completions, ) Disposer.dispose(states) @@ -613,7 +398,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val details = states.recommendationContext.details // set to true when at least one is not discarded or empty - val hasAtLeastOneValid = details.any { !it.isDiscarded && it.recommendation.content().isNotEmpty() } + val hasAtLeastOneValid = details.any { !it.isDiscarded && it.completion.insertText.isNotEmpty() } if (!hasAtLeastOneValid && showHint && states.requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { showCodeWhispererInfoHint( @@ -629,22 +414,16 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } private fun sendDiscardedUserDecisionEventForAll( - requestContext: RequestContext, - responseContext: ResponseContext, - recommendations: List, + project: Project, + latencyContext: LatencyContext, + sessionId: String, + completions: InlineCompletionListWithReferences, ) { - val detailContexts = recommendations.map { - DetailContext("", it, it, true, false, "", getCompletionType(it)) + val detailContexts = completions.items.map { + DetailContext(it.itemId, it, true, getCompletionType(it)) } - val recommendationContext = RecommendationContext(detailContexts, "", "", VisualPosition(0, 0)) - - CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll( - requestContext, - responseContext, - recommendationContext, - SessionContext(), - false - ) + val recommendationContext = RecommendationContext(detailContexts, "", VisualPosition(0, 0)) + CodeWhispererTelemetryService.getInstance().sendUserTriggerDecisionEvent(project, latencyContext, sessionId, recommendationContext) } fun getRequestContext( @@ -657,17 +436,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { // 1. file context val fileContext: FileContextInfo = runReadAction { FileContextProvider.getInstance(project).extractFileContext(editor, psiFile) } - // the upper bound for supplemental context duration is 50ms - // 2. supplemental context - val supplementalContext = cs.async { - try { - FileContextProvider.getInstance(project).extractSupplementalFileContext(psiFile, fileContext, timeout = SUPPLEMENTAL_CONTEXT_TIMEOUT) - } catch (e: Exception) { - LOG.warn { "Run into unexpected error when fetching supplemental context, error: ${e.message}" } - null - } - } - // 3. caret position val caretPosition = runReadAction { getCaretPosition(editor) } @@ -677,8 +445,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { // 5. customization val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn - val profileArn = QRegionProfileManager.getInstance().activeProfile(project)?.arn - var workspaceId: String? = null try { val workspacesInfos = getWorkspaceIds(project).get().workspaces @@ -693,24 +459,20 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } catch (e: Exception) { LOG.warn { "Cannot get workspaceId from LSP'$e'" } } - val diagnostics = getDocumentDiagnostics(editor.document, project) return RequestContext( project, editor, triggerTypeInfo, caretPosition, fileContext, - supplementalContext, connection, latencyContext, customizationArn, - profileArn, workspaceId, - diagnostics ) } - private fun getWorkspaceIds(project: Project): CompletableFuture { + fun getWorkspaceIds(project: Project): CompletableFuture { val payload = GetConfigurationFromServerParams( section = "aws.q.workspaceContext" ) @@ -719,25 +481,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) } - fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse { - // If contentSpans in reference are not consistent with content(recommendations), - // remove the incorrect references. - val validatedRecommendations = response.completions().map { - val validReferences = it.hasReferences() && it.references().isNotEmpty() && - it.references().none { reference -> - val span = reference.recommendationContentSpan() - span.start() > span.end() || span.start() < 0 || span.end() > it.content().length - } - if (validReferences) { - it - } else { - it.toBuilder().references(DefaultSdkAutoConstructList.getInstance()).build() - } - } - - return response.toBuilder().completions(validatedRecommendations).build() - } - private fun buildInvocationContext( requestContext: RequestContext, responseContext: ResponseContext, @@ -756,6 +499,33 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { return states } + fun createInlineCompletionParams( + editor: Editor, + triggerTypeInfo: TriggerTypeInfo, + nextToken: Either?, + ): InlineCompletionWithReferencesParams = + ReadAction.compute { + InlineCompletionWithReferencesParams( + context = InlineCompletionContext( + // Map the triggerTypeInfo to appropriate InlineCompletionTriggerKind + triggerKind = when (triggerTypeInfo.triggerType) { + CodewhispererTriggerType.OnDemand -> InlineCompletionTriggerKind.Invoke + CodewhispererTriggerType.AutoTrigger -> InlineCompletionTriggerKind.Automatic + else -> InlineCompletionTriggerKind.Invoke + } + ), + ).apply { + textDocument = TextDocumentIdentifier(toUriString(editor.virtualFile)) + position = Position( + editor.caretModel.primaryCaret.logicalPosition.line, + editor.caretModel.primaryCaret.logicalPosition.column + ) + if (nextToken != null) { + partialResultToken = nextToken + } + } + } + private fun addPopupChildDisposables(popup: JBPopup) { codeInsightSettingsFacade.disableCodeInsightUntil(popup) @@ -765,18 +535,16 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } private fun logServiceInvocation( - requestId: String, requestContext: RequestContext, responseContext: ResponseContext, - recommendations: List, + completion: InlineCompletionListWithReferences?, latency: Double?, exceptionType: String?, ) { - val recommendationLogs = recommendations.map { it.content().trimEnd() } - .reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } + val recommendationLogs = completion?.items?.map { it.insertText.trimEnd() } + ?.reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } LOG.info { "SessionId: ${responseContext.sessionId}, " + - "RequestId: $requestId, " + "Jetbrains IDE: ${ApplicationInfo.getInstance().fullApplicationName}, " + "IDE version: ${ApplicationInfo.getInstance().apiVersion}, " + "Filename: ${requestContext.fileContextInfo.filename}, " + @@ -831,11 +599,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { private val LOG = getLogger() private const val MAX_REFRESH_ATTEMPT = 3 - val CODEWHISPERER_CODE_COMPLETION_PERFORMED: Topic = Topic.create( - "CodeWhisperer code completion service invoked", - CodeWhispererCodeCompletionServiceListener::class.java - ) - fun getInstance(): CodeWhispererService = service() const val KET_SESSION_ID = "x-amzn-SessionId" private var reAuthPromptShown = false @@ -845,46 +608,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } fun hasReAuthPromptBeenShown() = reAuthPromptShown - - fun buildCodeWhispererRequest( - fileContextInfo: FileContextInfo, - supplementalContext: SupplementalContextInfo?, - customizationArn: String?, - profileArn: String?, - workspaceId: String?, - ): GenerateCompletionsRequest { - val programmingLanguage = ProgrammingLanguage.builder() - .languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) - .build() - val fileContext = FileContext.builder() - .leftFileContent(fileContextInfo.caretContext.leftFileContext) - .rightFileContent(fileContextInfo.caretContext.rightFileContext) - .filename(fileContextInfo.fileRelativePath ?: fileContextInfo.filename) - .fileUri(fileContextInfo.fileUri) - .programmingLanguage(programmingLanguage) - .build() - val supplementalContexts = supplementalContext?.contents?.map { - SupplementalContext.builder() - .content(it.content) - .filePath(it.path) - .build() - }.orEmpty() - val includeCodeWithReference = if (CodeWhispererSettings.getInstance().isIncludeCodeWithReference()) { - RecommendationsWithReferencesPreference.ALLOW - } else { - RecommendationsWithReferencesPreference.BLOCK - } - - return GenerateCompletionsRequest.builder() - .fileContext(fileContext) - .supplementalContexts(supplementalContexts) - .referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) } - .customizationArn(customizationArn) - .optOutPreference(getTelemetryOptOutPreference()) - .profileArn(profileArn) - .workspaceId(workspaceId) - .build() - } } } @@ -894,39 +617,12 @@ data class RequestContext( val triggerTypeInfo: TriggerTypeInfo, val caretPosition: CaretPosition, val fileContextInfo: FileContextInfo, - private val supplementalContextDeferred: Deferred, val connection: ToolkitConnection?, val latencyContext: LatencyContext, val customizationArn: String?, - val profileArn: String?, val workspaceId: String?, - val diagnostics: List?, -) { - // TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only - var supplementalContext: SupplementalContextInfo? = null - private set - get() = when (field) { - null -> { - if (!supplementalContextDeferred.isCompleted) { - error("attempt to access supplemental context before awaiting the deferred") - } else { - null - } - } - - else -> field - } - - suspend fun awaitSupplementalContext(): SupplementalContextInfo? { - supplementalContext = supplementalContextDeferred.await() - return supplementalContext - } -} +) data class ResponseContext( val sessionId: String, ) - -interface CodeWhispererCodeCompletionServiceListener { - fun onSuccess(fileContextInfo: FileContextInfo) {} -} 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 15003451181..234843e417d 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 @@ -4,10 +4,9 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service import com.intellij.codeInsight.hint.HintManager -import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service @@ -23,74 +22,56 @@ import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.messages.Either import software.amazon.awssdk.core.exception.SdkServiceException -import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.FileContext -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage -import software.amazon.awssdk.services.codewhispererruntime.model.RecommendationsWithReferencesPreference -import software.amazon.awssdk.services.codewhispererruntime.model.ResourceNotFoundException -import software.amazon.awssdk.services.codewhispererruntime.model.SupplementalContext import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException 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.coroutines.getCoroutineBgContext import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope 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.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionContext +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionTriggerKind +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionWithReferencesParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil.toUriString import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManagerNew import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.getCaretPosition -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.isSupportedJsonFormat import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJson import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew +import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.WorkerContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService.Companion.CODEWHISPERER_CODE_COMPLETION_PERFORMED -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeInsightsSettingsFacade import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.isInjectedText import software.aws.toolkits.jetbrains.utils.isQExpired -import software.aws.toolkits.jetbrains.utils.notifyWarn 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.util.concurrent.TimeUnit @@ -131,8 +112,6 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val project = editor.project ?: return if (!isCodeWhispererEnabled(project)) return - latencyContext.credentialFetchingStart = System.nanoTime() - // try to refresh automatically if possible, otherwise ask user to login again if (isQExpired(project)) { // consider changing to only running once a ~minute since this is relatively expensive @@ -156,7 +135,6 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } } - latencyContext.credentialFetchingEnd = System.nanoTime() val psiFile = runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(editor.document) } if (psiFile == null) { @@ -174,7 +152,6 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { getRequestContext(triggerTypeInfo, editor, project, psiFile) } catch (e: Exception) { LOG.debug { e.message.toString() } - CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) return } val caretContext = requestContext.fileContextInfo.caretContext @@ -186,25 +163,6 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } } - val language = requestContext.fileContextInfo.programmingLanguage - val leftContext = requestContext.fileContextInfo.caretContext.leftFileContext - if (!language.isCodeCompletionSupported() || ( - language is CodeWhispererJson && !isSupportedJsonFormat( - requestContext.fileContextInfo.filename, - leftContext - ) - ) - ) { - LOG.debug { "Programming language $language is not supported by CodeWhisperer" } - if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { - showCodeWhispererInfoHint( - requestContext.editor, - message("codewhisperer.language.error", psiFile.fileType.name) - ) - } - return - } - LOG.debug { "Calling CodeWhisperer service, jobId: $currentJobId, trigger type: ${triggerTypeInfo.triggerType}" + if (triggerTypeInfo.triggerType == CodewhispererTriggerType.AutoTrigger) { @@ -240,182 +198,102 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { var lastRecommendationIndex = -1 try { - val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( - buildCodeWhispererRequest( - requestContext.fileContextInfo, - requestContext.awaitSupplementalContext(), - requestContext.customizationArn, - requestContext.profileArn - ) - ) - var startTime = System.nanoTime() - latencyContext.codewhispererPreprocessingEnd = System.nanoTime() - latencyContext.paginationAllCompletionsStart = System.nanoTime() CodeWhispererInvocationStatusNew.getInstance().setInvocationStart() var requestCount = 0 - for (response in responseIterable) { - requestCount++ - val endTime = System.nanoTime() - val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble() - startTime = endTime - val requestId = response.responseMetadata().requestId() - val sessionId = response.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] - if (requestCount == 1) { - latencyContext.codewhispererPostprocessingStart = System.nanoTime() - latencyContext.paginationFirstCompletionTime = latency - latencyContext.firstRequestId = requestId - CodeWhispererInvocationStatusNew.getInstance().setInvocationSessionId(sessionId) - } - if (response.nextToken().isEmpty()) { - latencyContext.paginationAllCompletionsEnd = System.nanoTime() + var nextToken: Either? = null + do { + val result = AmazonQLspService.executeIfRunning(requestContext.project) { server -> + val params = createInlineCompletionParams(requestContext.editor, requestContext.triggerTypeInfo, nextToken) + server.inlineCompletionWithReferences(params) } - val responseContext = ResponseContext(sessionId) - logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null) - lastRecommendationIndex += response.completions().size - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) - .onSuccess(requestContext.fileContextInfo) - CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent( - currentJobId, - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - true, - latency, - null - ) + result?.thenAccept { completion -> + nextToken = completion.partialResultToken + requestCount++ + val endTime = System.nanoTime() + val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble() + startTime = endTime + val responseContext = ResponseContext(completion.sessionId) + logServiceInvocation(requestContext, responseContext, completion, latency, null) + lastRecommendationIndex += completion.items.size - val validatedResponse = validateResponse(response) - - runInEdt { - // If delay is not met, add them to the worker queue and process them later. - // On first response, workers queue must be empty. If there's enough delay before showing, - // process CodeWhisperer UI rendering and workers queue will remain empty throughout this - // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task - // will be added to the workers queue. - // On subsequent responses, if they see workers queue is not empty, it means the first worker - // task hasn't been finished yet, in this case simply add another task to the queue. If they - // see worker queue is empty, the previous tasks must have been finished before this. In this - // case render CodeWhisperer UI directly. - val workerContext = WorkerContextNew(requestContext, responseContext, validatedResponse) - if (workerContexts.isNotEmpty()) { - workerContexts.add(workerContext) - } else { - if (ongoingRequests.values.filterNotNull().isEmpty() && - !CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer() - ) { - // It's the first response, and no enough delay before showing - projectCoroutineScope(requestContext.project).launch { - while (!CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer()) { - delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL) - } - runInEdt { - workerContexts.forEach { - processCodeWhispererUI( - sessionContext, - it, - ongoingRequests[currentJobId], - cs, - currentJobId - ) - if (!ongoingRequests.contains(currentJobId)) { - job?.cancel() + runInEdt { + // If delay is not met, add them to the worker queue and process them later. + // On first response, workers queue must be empty. If there's enough delay before showing, + // process CodeWhisperer UI rendering and workers queue will remain empty throughout this + // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task + // will be added to the workers queue. + // On subsequent responses, if they see workers queue is not empty, it means the first worker + // task hasn't been finished yet, in this case simply add another task to the queue. If they + // see worker queue is empty, the previous tasks must have been finished before this. In this + // case render CodeWhisperer UI directly. + val workerContext = WorkerContextNew(requestContext, responseContext, completion) + if (workerContexts.isNotEmpty()) { + workerContexts.add(workerContext) + } else { + if (ongoingRequests.values.filterNotNull().isEmpty() && + !CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer() + ) { + // It's the first response, and no enough delay before showing + projectCoroutineScope(requestContext.project).launch { + while (!CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer()) { + delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL) + } + runInEdt { + workerContexts.forEach { + processCodeWhispererUI( + sessionContext, + it, + ongoingRequests[currentJobId], + cs, + currentJobId + ) + if (!ongoingRequests.contains(currentJobId)) { + job?.cancel() + } } + workerContexts.clear() } - workerContexts.clear() } - } - workerContexts.add(workerContext) - } else { - // Have enough delay before showing for the first response, or it's subsequent responses - processCodeWhispererUI( - sessionContext, - workerContext, - ongoingRequests[currentJobId], - cs, - currentJobId - ) - if (!ongoingRequests.contains(currentJobId)) { - job?.cancel() + workerContexts.add(workerContext) + } else { + // Have enough delay before showing for the first response, or it's subsequent responses + processCodeWhispererUI( + sessionContext, + workerContext, + ongoingRequests[currentJobId], + cs, + currentJobId + ) + if (!ongoingRequests.contains(currentJobId)) { + job?.cancel() + } } } } + if (!cs.isActive) { + // If job is cancelled before we do another request, don't bother making + // another API call to save resources + LOG.debug { "Skipping sending remaining requests on inactive CodeWhisperer session exit" } + return@thenAccept + } + if (requestCount >= PAGINATION_REQUEST_COUNT_ALLOWED) { + LOG.debug { "Only $PAGINATION_REQUEST_COUNT_ALLOWED request per pagination session for now" } + CodeWhispererInvocationStatusNew.getInstance().finishInvocation() + return@thenAccept + } } - if (!cs.isActive) { - // If job is cancelled before we do another request, don't bother making - // another API call to save resources - LOG.debug { "Skipping sending remaining requests on inactive CodeWhisperer session exit" } - return - } - if (requestCount >= PAGINATION_REQUEST_COUNT_ALLOWED) { - LOG.debug { "Only $PAGINATION_REQUEST_COUNT_ALLOWED request per pagination session for now" } - CodeWhispererInvocationStatusNew.getInstance().finishInvocation() - break - } - } + } while (nextToken != null) } catch (e: Exception) { val requestId: String val sessionId: String val displayMessage: String - if ( - CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) || - e is ResourceNotFoundException - ) { - (e as CodeWhispererRuntimeException) - - requestId = e.requestId().orEmpty() - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] - val exceptionType = e::class.simpleName - val responseContext = ResponseContext(sessionId) - - CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent( - currentJobId, - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - false, - 0.0, - exceptionType - ) - - LOG.debug { - "The provided customization ${requestContext.customizationArn} is not found, " + - "will fallback to the default and retry generate completion" - } - logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType) - - notifyWarn( - title = "", - content = message("codewhisperer.notification.custom.not_available"), - project = requestContext.project, - notificationActions = listOf( - NotificationAction.create( - message("codewhisperer.notification.custom.simple.button.select_another_customization") - ) { _, notification -> - CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project) - notification.expire() - } - ) - ) - CodeWhispererInvocationStatusNew.getInstance().finishInvocation() - - requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) } - - showRecommendationsInPopup( - requestContext.editor, - requestContext.triggerTypeInfo, - latencyContext - ) - return - } else if (e is CodeWhispererRuntimeException) { + if (e is CodeWhispererRuntimeException) { requestId = e.requestId().orEmpty() sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side") } else { - requestId = "" sessionId = "" val statusCode = if (e is SdkServiceException) e.statusCode() else 0 displayMessage = @@ -431,17 +309,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val exceptionType = e::class.simpleName val responseContext = ResponseContext(sessionId) CodeWhispererInvocationStatusNew.getInstance().setInvocationSessionId(sessionId) - logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType) - CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent( - currentJobId, - requestId, - requestContext, - responseContext, - lastRecommendationIndex, - false, - 0.0, - exceptionType - ) + logServiceInvocation(requestContext, responseContext, null, null, exceptionType) if (e is ThrottlingException && e.message == CodeWhispererConstants.THROTTLING_MESSAGE @@ -478,20 +346,19 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { ) { val requestContext = workerContext.requestContext val responseContext = workerContext.responseContext - val response = workerContext.response - val requestId = response.responseMetadata().requestId() + val completions = workerContext.completions // At this point when we are in EDT, the state of the popup will be thread-safe // across this thread execution, so if popup is disposed, we will stop here. // This extra check is needed because there's a time between when we get the response and // when we enter the EDT. if (!coroutine.isActive || sessionContext.isDisposed()) { - LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. RequestId: $requestId, jobId: $jobId" } + LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. session id: ${completions.sessionId}, jobId: $jobId" } return } if (requestContext.editor.isDisposed) { - LOG.debug { "Stop showing all CodeWhisperer recommendations since editor is disposed. RequestId: $requestId, jobId: $jobId" } + LOG.debug { "Stop showing all CodeWhisperer recommendations since editor is disposed. session id: ${completions.sessionId}, jobId: $jobId" } disposeDisplaySession(false) return } @@ -506,7 +373,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val nextStates: InvocationContextNew? if (currStates == null) { // first response for the jobId - nextStates = initStates(jobId, requestContext, responseContext, response, caretMovement) + nextStates = initStates(jobId, requestContext, responseContext, completions, caretMovement) // receiving a null state means caret has moved backward, // so we are going to cancel the current job @@ -515,9 +382,9 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } } else { // subsequent responses for the jobId - nextStates = updateStates(currStates, response) + nextStates = updateStates(currStates, completions) } - LOG.debug { "Adding ${response.completions().size} completions to the session. RequestId: $requestId, jobId: $jobId" } + LOG.debug { "Adding ${completions.items.size} completions to the session. session id: ${completions.sessionId}, jobId: $jobId" } // TODO: may have bug when it's a mix of auto-trigger + manual trigger val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, true) @@ -529,23 +396,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { // since it won't be sent automatically later // TODO: may have bug; visit later if (nextStates.recommendationContext.details.isEmpty()) { - LOG.debug { "Received just an empty list from this session, requestId: $requestId" } - CodeWhispererTelemetryServiceNew.getInstance().sendUserDecisionEvent( - requestContext, - responseContext, - DetailContextNew( - requestId, - Completion.builder().build(), - Completion.builder().build(), - false, - false, - "", - CodewhispererCompletionType.Line - ), - -1, - CodewhispererSuggestionState.Empty, - nextStates.recommendationContext.details.size - ) + LOG.debug { "Received just an empty list from this session. session id: ${completions.sessionId}" } } if (!hasAtLeastOneValid) { LOG.debug { "None of the recommendations are valid, exiting current CodeWhisperer pagination session" } @@ -565,62 +416,50 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { jobId: Int, requestContext: RequestContextNew, responseContext: ResponseContext, - response: GenerateCompletionsResponse, + completions: InlineCompletionListWithReferences, caretMovement: CaretMovement, ): InvocationContextNew? { - val requestId = response.responseMetadata().requestId() - val recommendations = response.completions() val visualPosition = requestContext.editor.caretModel.visualPosition if (caretMovement == CaretMovement.MOVE_BACKWARD) { - LOG.debug { "Caret moved backward, discarding all of the recommendations and exiting the session. Request ID: $requestId, jobId: $jobId" } - val detailContexts = recommendations.map { - DetailContextNew("", it, it, true, false, "", getCompletionType(it)) + LOG.debug { "Caret moved backward, discarding all of the recommendations. Session Id: ${completions.sessionId}, jobId: $jobId" } + val detailContexts = completions.items.map { + DetailContext("", it, true, getCompletionType(it)) }.toMutableList() - val recommendationContext = RecommendationContextNew(detailContexts, "", "", VisualPosition(0, 0), jobId) + val recommendationContext = RecommendationContextNew(detailContexts, "", VisualPosition(0, 0), jobId) ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext) disposeDisplaySession(false) return null } - val userInputOriginal = CodeWhispererEditorManagerNew.getInstance().getUserInputSinceInvocation( - requestContext.editor, - requestContext.caretPosition.offset - ) val userInput = if (caretMovement == CaretMovement.NO_CHANGE) { - LOG.debug { "Caret position not changed since invocation. Request ID: $requestId" } + LOG.debug { "Caret position not changed since invocation. Session Id: ${completions.sessionId}" } "" } else { - userInputOriginal.trimStart().also { - LOG.debug { - "Caret position moved forward since invocation. Request ID: $requestId, " + - "user input since invocation: $userInputOriginal, " + - "user input without leading spaces: $it" - } - } + LOG.debug { "Caret position moved forward since invocation. Session Id: ${completions.sessionId}" } + CodeWhispererEditorManagerNew.getInstance().getUserInputSinceInvocation( + requestContext.editor, + requestContext.caretPosition.offset + ) } val detailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - requestContext, userInput, - recommendations, - requestId + completions, ) - val recommendationContext = RecommendationContextNew(detailContexts, userInputOriginal, userInput, visualPosition, jobId) + val recommendationContext = RecommendationContextNew(detailContexts, userInput, visualPosition, jobId) ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext) return ongoingRequests[jobId] } private fun updateStates( states: InvocationContextNew, - response: GenerateCompletionsResponse, + completions: InlineCompletionListWithReferences, ): InvocationContextNew { val recommendationContext = states.recommendationContext val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - states.requestContext, - recommendationContext.userInputSinceInvocation, - response.completions(), - response.responseMetadata().requestId() + recommendationContext.userInput, + completions, ) recommendationContext.details.addAll(newDetailContexts) @@ -632,7 +471,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val details = states.recommendationContext.details // set to true when at least one is not discarded or empty - val hasAtLeastOneValid = details.any { !it.isDiscarded && it.recommendation.content().isNotEmpty() } + val hasAtLeastOneValid = details.any { !it.isDiscarded && it.completion.insertText.isNotEmpty() } if (!hasAtLeastOneValid && showHint && states.requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { showCodeWhispererInfoHint( @@ -674,7 +513,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { ongoingRequests.values.filterNotNull().flatMap { element -> val context = element.recommendationContext context.details.map { - PreviewContext(context.jobId, it, context.userInputSinceInvocation, context.typeahead) + PreviewContext(context.jobId, it, context.userInput, context.typeahead) } } @@ -689,48 +528,13 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { // 1. file context val fileContext: FileContextInfo = runReadAction { FileContextProvider.getInstance(project).extractFileContext(editor, psiFile) } - // the upper bound for supplemental context duration is 50ms - // 2. supplemental context - val supplementalContext = cs.async { - try { - FileContextProvider.getInstance(project).extractSupplementalFileContext(psiFile, fileContext, timeout = SUPPLEMENTAL_CONTEXT_TIMEOUT) - } catch (e: Exception) { - LOG.warn { "Run into unexpected error when fetching supplemental context, error: ${e.message}" } - null - } - } - // 3. caret position val caretPosition = runReadAction { getCaretPosition(editor) } // 4. connection val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(CodeWhispererConnection.getInstance()) - // 5. customization - val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn - - val profileArn = QRegionProfileManager.getInstance().activeProfile(project)?.arn - - return RequestContextNew(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, customizationArn, profileArn) - } - - fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse { - // If contentSpans in reference are not consistent with content(recommendations), - // remove the incorrect references. - val validatedRecommendations = response.completions().map { - val validReferences = it.hasReferences() && it.references().isNotEmpty() && - it.references().none { reference -> - val span = reference.recommendationContentSpan() - span.start() > span.end() || span.start() < 0 || span.end() > it.content().length - } - if (validReferences) { - it - } else { - it.toBuilder().references(DefaultSdkAutoConstructList.getInstance()).build() - } - } - - return response.toBuilder().completions(validatedRecommendations).build() + return RequestContextNew(project, editor, triggerTypeInfo, caretPosition, fileContext, connection) } private fun buildInvocationContext( @@ -750,19 +554,44 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { return states } + private fun createInlineCompletionParams( + editor: Editor, + triggerTypeInfo: TriggerTypeInfo, + nextToken: Either?, + ): InlineCompletionWithReferencesParams = + ReadAction.compute { + InlineCompletionWithReferencesParams( + context = InlineCompletionContext( + // Map the triggerTypeInfo to appropriate InlineCompletionTriggerKind + triggerKind = when (triggerTypeInfo.triggerType) { + CodewhispererTriggerType.OnDemand -> InlineCompletionTriggerKind.Invoke + CodewhispererTriggerType.AutoTrigger -> InlineCompletionTriggerKind.Automatic + else -> InlineCompletionTriggerKind.Invoke + } + ), + ).apply { + textDocument = TextDocumentIdentifier(toUriString(editor.virtualFile)) + position = Position( + editor.caretModel.primaryCaret.logicalPosition.line, + editor.caretModel.primaryCaret.logicalPosition.column + ) + if (nextToken != null) { + workDoneToken = nextToken + } + } + } + private fun logServiceInvocation( - requestId: String, requestContext: RequestContextNew, responseContext: ResponseContext, - recommendations: List, + completions: InlineCompletionListWithReferences?, latency: Double?, exceptionType: String?, ) { - val recommendationLogs = recommendations.map { it.content().trimEnd() } - .reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } + val recommendationLogs = completions?.items?.map { it.insertText.trimEnd() } + ?.reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } LOG.info { "SessionId: ${responseContext.sessionId}, " + - "RequestId: $requestId, " + "Jetbrains IDE: ${ApplicationInfo.getInstance().fullApplicationName}, " + "IDE version: ${ApplicationInfo.getInstance().apiVersion}, " + "Filename: ${requestContext.fileContextInfo.filename}, " + @@ -817,51 +646,6 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { fun getInstance(): CodeWhispererServiceNew = service() const val KET_SESSION_ID = "x-amzn-SessionId" - private var reAuthPromptShown = false - - fun markReAuthPromptShown() { - reAuthPromptShown = true - } - - fun hasReAuthPromptBeenShown() = reAuthPromptShown - - fun buildCodeWhispererRequest( - fileContextInfo: FileContextInfo, - supplementalContext: SupplementalContextInfo?, - customizationArn: String?, - profileArn: String?, - ): GenerateCompletionsRequest { - val programmingLanguage = ProgrammingLanguage.builder() - .languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) - .build() - val fileContext = FileContext.builder() - .leftFileContent(fileContextInfo.caretContext.leftFileContext) - .rightFileContent(fileContextInfo.caretContext.rightFileContext) - .filename(fileContextInfo.fileRelativePath ?: fileContextInfo.filename) - .fileUri(fileContextInfo.fileUri) - .programmingLanguage(programmingLanguage) - .build() - val supplementalContexts = supplementalContext?.contents?.map { - SupplementalContext.builder() - .content(it.content) - .filePath(it.path) - .build() - }.orEmpty() - val includeCodeWithReference = if (CodeWhispererSettings.getInstance().isIncludeCodeWithReference()) { - RecommendationsWithReferencesPreference.ALLOW - } else { - RecommendationsWithReferencesPreference.BLOCK - } - - return GenerateCompletionsRequest.builder() - .fileContext(fileContext) - .supplementalContexts(supplementalContexts) - .referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) } - .customizationArn(customizationArn) - .optOutPreference(getTelemetryOptOutPreference()) - .profileArn(profileArn) - .build() - } } } @@ -871,30 +655,8 @@ data class RequestContextNew( val triggerTypeInfo: TriggerTypeInfo, val caretPosition: CaretPosition, val fileContextInfo: FileContextInfo, - 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 - private set - get() = when (field) { - null -> { - if (!supplementalContextDeferred.isCompleted) { - error("attempt to access supplemental context before awaiting the deferred") - } else { - null - } - } - else -> field - } - - suspend fun awaitSupplementalContext(): SupplementalContextInfo? { - supplementalContext = supplementalContextDeferred.await() - return supplementalContext - } -} +) interface CodeWhispererIntelliSenseOnHoverListener { fun onEnter() {} 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 77f901950c7..4a8d97575a5 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 @@ -11,6 +11,7 @@ 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.project.ProjectManager import com.intellij.openapi.ui.emptyText import com.intellij.ui.components.ActionLink import com.intellij.ui.components.fields.ExpandableTextField @@ -23,6 +24,7 @@ import com.intellij.util.concurrency.EdtExecutorService import com.intellij.util.execution.ParametersListUtil import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener +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.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled @@ -81,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) : .resizableColumn() .align(Align.FILL) } + row(message("amazonqFeatureDev.placeholder.node_runtime_path")) { + val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() + fileChooserDescriptor.isForcedToUseIdeaFileChooser = true + + textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor) + .bindText( + { LspSettings.getInstance().getNodeRuntimePath().orEmpty() }, + { LspSettings.getInstance().setNodeRuntimePath(it) } + ) + .applyToComponent { + emptyText.text = message("executableCommon.auto_managed") + } + .resizableColumn() + .align(Align.FILL) + } } group(message("aws.settings.codewhisperer.group.general")) { @@ -284,6 +301,20 @@ class CodeWhispererConfigurable(private val project: Project) : } } } + }.also { + val newCallbacks = it.applyCallbacks.toMutableMap() + .also { map -> + val list = map.getOrPut(null) { mutableListOf() } as MutableList<() -> Unit> + list.add { + ProjectManager.getInstance().openProjects.forEach { project -> + if (project.isDisposed) { + return@forEach + } + AmazonQLspService.didChangeConfiguration(project) + } + } + } + it.applyCallbacks = newCallbacks } companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt index 5500f29204a..4609e14cdcf 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt @@ -29,7 +29,7 @@ object CodeWhispererIntelliSenseAutoTriggerListener : LookupManagerListener { } // Classifier - CodeWhispererAutoTriggerService.getInstance().tryInvokeAutoTrigger(editor, CodeWhispererAutomatedTriggerType.IntelliSense()) + CodeWhispererAutoTriggerService.getInstance().invoke(editor, CodeWhispererAutomatedTriggerType.IntelliSense()) cleanup() } override fun lookupCanceled(event: LookupEvent) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt deleted file mode 100644 index 46bcc935743..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Key -import com.intellij.refactoring.suggested.range -import com.intellij.util.Alarm -import com.intellij.util.AlarmFactory -import info.debatty.java.stringsimilarity.Levenshtein -import org.jetbrains.annotations.TestOnly -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -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.codewhisperer.credentials.CodeWhispererClientAdaptor -import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererCodeCompletionServiceListener -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_SECONDS_IN_MINUTE -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getUnmodifiedAcceptedCharsCount -import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled -import software.aws.toolkits.telemetry.CodewhispererTelemetry -import java.time.Duration -import java.time.Instant -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.roundToLong - -// TODO: reset code coverage calculator on logging out connection? -// TODO: rename "Tokens" to "Characters", and many more renames in this file -abstract class CodeWhispererCodeCoverageTracker( - private val project: Project, - private val timeWindowInSec: Long, - private val language: CodeWhispererProgrammingLanguage, - private val rangeMarkers: MutableList, - private val fileToTokens: MutableMap, - private val myServiceInvocationCount: AtomicInteger, -) : Disposable { - val percentage: Long? - get() = if (totalCharsCount != 0L) calculatePercentage(acceptedCharsCount, totalCharsCount) else null - val unmodifiedAcceptedCharsCount: Long - get() = fileToTokens.map { - it.value.unmodifiedAcceptedChars.get() - }.fold(0) { acc, next -> - acc + next - } - val totalCharsCount: Long - get() = fileToTokens.map { - it.value.totalChars.get() - }.fold(0) { acc, next -> - acc + next - } - private val acceptedCharsCount: Long - get() = fileToTokens.map { - it.value.acceptedChars.get() - }.fold(0) { acc, next -> - acc + next - } - val acceptedRecommendationsCount: Int - get() = rangeMarkers.size - val serviceInvocationCount: Int - get() = myServiceInvocationCount.get() - private val isActive: AtomicBoolean = AtomicBoolean(false) - private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this) - private val isShuttingDown = AtomicBoolean(false) - private var startTime: Instant = Instant.now() - - @Synchronized - fun activateTrackerIfNotActive() { - // tracker will only be activated if and only if IsTelemetryEnabled = true && isActive = false - if (!isTelemetryEnabled() || isActive.getAndSet(true)) return - - val conn = ApplicationManager.getApplication().messageBus.connect() - if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) { - conn.subscribe( - CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED, - object : CodeWhispererUserActionListener { - override fun afterAccept( - states: InvocationContextNew, - previews: List, - sessionContext: SessionContextNew, - rangeMarker: RangeMarker, - ) { - if (states.requestContext.fileContextInfo.programmingLanguage != language) return - rangeMarkers.add(rangeMarker) - val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return - rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation) - runReadAction { - // also increment total tokens because accepted tokens are part of it - incrementTotalCharsCount(rangeMarker.document, originalRecommendation.length) - // avoid counting CodeWhisperer inserted suggestion twice in total tokens - if (rangeMarker.textRange.length in 2..49 && originalRecommendation.trim().isNotEmpty()) { - incrementTotalCharsCount(rangeMarker.document, -rangeMarker.textRange.length) - } - } - } - } - ) - } else { - conn.subscribe( - CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED, - object : CodeWhispererUserActionListener { - override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) { - if (states.requestContext.fileContextInfo.programmingLanguage != language) return - rangeMarkers.add(rangeMarker) - val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return - rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation) - runReadAction { - // also increment total tokens because accepted tokens are part of it - incrementTotalCharsCount(rangeMarker.document, originalRecommendation.length) - // avoid counting CodeWhisperer inserted suggestion twice in total tokens - if (rangeMarker.textRange.length in 2..49 && originalRecommendation.trim().isNotEmpty()) { - incrementTotalCharsCount(rangeMarker.document, -rangeMarker.textRange.length) - } - } - } - } - ) - } - - conn.subscribe( - CodeWhispererService.CODEWHISPERER_CODE_COMPLETION_PERFORMED, - object : CodeWhispererCodeCompletionServiceListener { - override fun onSuccess(fileContextInfo: FileContextInfo) { - if (language == fileContextInfo.programmingLanguage) { - myServiceInvocationCount.getAndIncrement() - } - } - } - ) - startTime = Instant.now() - scheduleCodeWhispererCodeCoverageTracker() - } - - fun isTrackerActive() = isActive.get() - - internal fun documentChanged(event: DocumentEvent) { - // When open a file for the first time, IDE will also emit DocumentEvent for loading with `isWholeTextReplaced = true` - // Added this condition to filter out those events - if (event.isWholeTextReplaced) { - LOG.debug { "event with isWholeTextReplaced flag: $event" } - if (event.oldTimeStamp == 0L) return - } - // only count total tokens when it is a user keystroke input - // do not count doc changes from copy & paste of >=50 characters - // do not count other changes from formatter, git command, etc - // edge case: event can be from user hit enter with indentation where change is \n\t\t, count as 1 char increase in total chars - // when event is auto closing [{(', there will be 2 separated events, both count as 1 char increase in total chars - val text = event.newFragment.toString() - if ((event.newLength == 1 && event.oldLength == 0) || (text.startsWith('\n') && text.trim().isEmpty())) { - incrementTotalCharsCount(event.document, 1) - return - } else if (event.newLength < 50 && text.trim().isNotEmpty()) { - // count doc changes from <50 multi character input as total user written code - // ignore all white space changes, this usually comes from IntelliJ formatting - incrementTotalCharsCount(event.document, event.newLength) - } - } - - internal fun extractRangeMarkerString(rangeMarker: RangeMarker): String? = runReadAction { - rangeMarker.range?.let { myRange -> rangeMarker.document.getText(myRange) } - } - - private fun flush() { - try { - if (isTelemetryEnabled()) emitCodeWhispererCodeContribution() - } finally { - reset() - scheduleCodeWhispererCodeCoverageTracker() - } - } - - private fun scheduleCodeWhispererCodeCoverageTracker() { - if (!alarm.isDisposed && !isShuttingDown.get()) { - alarm.addRequest({ flush() }, Duration.ofSeconds(timeWindowInSec).toMillis()) - } - } - - private fun incrementUnmodifiedAcceptedCharsCount(document: Document, delta: Int) { - val tokens = fileToTokens.getOrPut(document) { CodeCoverageTokens() } - tokens.unmodifiedAcceptedChars.addAndGet(delta) - } - - private fun incrementAcceptedCharsCount(document: Document, delta: Int) { - val tokens = fileToTokens.getOrPut(document) { CodeCoverageTokens() } - tokens.acceptedChars.addAndGet(delta) - } - - private fun incrementTotalCharsCount(document: Document, delta: Int) { - val tokens = fileToTokens.getOrPut(document) { CodeCoverageTokens() } - tokens.apply { - totalChars.addAndGet(delta) - if (totalChars.get() < 0) totalChars.set(0) - } - } - - private fun reset() { - startTime = Instant.now() - rangeMarkers.clear() - fileToTokens.clear() - myServiceInvocationCount.set(0) - } - - internal fun emitCodeWhispererCodeContribution() { - // If the user is inactive or did not invoke, don't emit the telemetry - if (percentage == null) return - if (myServiceInvocationCount.get() <= 0) return - - rangeMarkers.forEach { rangeMarker -> - if (!rangeMarker.isValid) return@forEach - // if users add more code upon the recommendation generated from CodeWhisperer, we consider those added part as userToken but not CwsprTokens - val originalRecommendation = rangeMarker.getUserData(KEY_REMAINING_RECOMMENDATION) - val modifiedRecommendation = extractRangeMarkerString(rangeMarker) - if (originalRecommendation == null || modifiedRecommendation == null) { - LOG.debug { - "failed to get accepted recommendation. " + - "OriginalRecommendation is null: ${originalRecommendation == null}; " + - "ModifiedRecommendation is null: ${modifiedRecommendation == null}" - } - return@forEach - } - val unmodifiedRecommendationLength = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - runReadAction { - incrementAcceptedCharsCount(rangeMarker.document, originalRecommendation.length) - incrementUnmodifiedAcceptedCharsCount(rangeMarker.document, unmodifiedRecommendationLength) - } - } - val customizationArn: String? = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn - - runIfIdcConnectionOrTelemetryEnabled(project) { - // here acceptedTokensSize is the count of accepted chars post user modification - try { - val response = CodeWhispererClientAdaptor.getInstance(project).sendCodePercentageTelemetry( - language, - customizationArn, - acceptedCharsCount, - totalCharsCount, - unmodifiedAcceptedCharsCount, - 0, - 0 - ) - LOG.debug { "Successfully sent code percentage telemetry. RequestId: ${response.responseMetadata().requestId()}" } - } catch (e: Exception) { - val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null - LOG.debug { - "Failed to send code percentage telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" - } - } - } - - // percentage == null means totalTokens == 0 and users are not editing the document, thus we shouldn't emit telemetry for this - percentage?.let { percentage -> - CodewhispererTelemetry.codePercentage( - project = null, - codewhispererAcceptedTokens = unmodifiedAcceptedCharsCount, - codewhispererSuggestedTokens = acceptedCharsCount, - codewhispererLanguage = language.toTelemetryType(), - codewhispererPercentage = percentage, - codewhispererTotalTokens = totalCharsCount, - successCount = myServiceInvocationCount.get().toLong(), - codewhispererCustomizationArn = customizationArn, - credentialStartUrl = getCodeWhispererStartUrl(project) - ) - } - } - - @TestOnly - fun forceTrackerFlush() { - alarm.drainRequestsInTest() - } - - @TestOnly - fun activeRequestCount() = alarm.activeRequestCount - - override fun dispose() { - if (isShuttingDown.getAndSet(true)) { - return - } - flush() - } - - companion object { - @JvmStatic - val levenshteinChecker = Levenshtein() - private const val REMAINING_RECOMMENDATION = "remainingRecommendation" - private val KEY_REMAINING_RECOMMENDATION = Key(REMAINING_RECOMMENDATION) - private val LOG = getLogger() - private val instances: MutableMap = mutableMapOf() - - fun calculatePercentage(rawAcceptedTokenSize: Long, totalTokens: Long): Long = ((rawAcceptedTokenSize.toDouble() * 100) / totalTokens).roundToLong() - fun getInstance(project: Project, language: CodeWhispererProgrammingLanguage): CodeWhispererCodeCoverageTracker = - when (val instance = instances[language]) { - null -> { - val newTracker = DefaultCodeWhispererCodeCoverageTracker(project, language) - instances[language] = newTracker - newTracker - } - else -> instance - } - - @TestOnly - fun getInstancesMap(): MutableMap { - assert(ApplicationManager.getApplication().isUnitTestMode) - return instances - } - } -} - -class DefaultCodeWhispererCodeCoverageTracker(project: Project, language: CodeWhispererProgrammingLanguage) : CodeWhispererCodeCoverageTracker( - project, - 5 * TOTAL_SECONDS_IN_MINUTE, - language, - mutableListOf(), - mutableMapOf(), - AtomicInteger(0) -) - -class CodeCoverageTokens(totalChars: Int = 0, unmodifiedAcceptedChars: Int = 0, acceptedChars: Int = 0) { - val totalChars = AtomicInteger(totalChars) - val unmodifiedAcceptedChars = AtomicInteger(unmodifiedAcceptedChars) - val acceptedChars = AtomicInteger(acceptedChars) -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt index 8fdd46d525c..ff81a6888c8 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt @@ -3,286 +3,65 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.editor.RangeMarker import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import kotlinx.coroutines.launch -import org.apache.commons.collections4.queue.CircularFifoQueue -import org.jetbrains.annotations.TestOnly -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope -import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.InlineCompletionStates +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LogInlineCompletionSessionResultsParams import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanTelemetryEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getGettingStartedTaskType -import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.telemetry.CodeFixAction import software.aws.toolkits.telemetry.CodewhispererCodeScanScope -import software.aws.toolkits.telemetry.CodewhispererCompletionType import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask -import software.aws.toolkits.telemetry.CodewhispererLanguage -import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState -import software.aws.toolkits.telemetry.CodewhispererSuggestionState import software.aws.toolkits.telemetry.CodewhispererTelemetry -import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Component import software.aws.toolkits.telemetry.CredentialSourceId import software.aws.toolkits.telemetry.MetricResult import software.aws.toolkits.telemetry.Result -import java.nio.file.Path import java.time.Duration import java.time.Instant -import java.util.Queue -import kotlin.io.path.pathString @Service class CodeWhispererTelemetryService { - // store previous 5 userTrigger decisions - private val previousUserTriggerDecisions = CircularFifoQueue(5) - - private var previousUserTriggerDecisionTimestamp: Instant? = null - - private val codewhispererTimeSinceLastUserDecision: Double? - get() { - return previousUserTriggerDecisionTimestamp?.let { - Duration.between(it, Instant.now()).toMillis().toDouble() - } - } - - val previousUserTriggerDecision: CodewhispererPreviousSuggestionState? - get() = if (previousUserTriggerDecisions.isNotEmpty()) previousUserTriggerDecisions.last() else null - companion object { fun getInstance(): CodeWhispererTelemetryService = service() val LOG = getLogger() - const val NO_ACCEPTED_INDEX = -1 - } - - fun sendFailedServiceInvocationEvent(project: Project, exceptionType: String?) { - CodewhispererTelemetry.serviceInvocation( - project = project, - codewhispererCursorOffset = 0, - codewhispererLanguage = CodewhispererLanguage.Unknown, - codewhispererLastSuggestionIndex = -1, - codewhispererLineNumber = 0, - codewhispererTriggerType = CodewhispererTriggerType.Unknown, - duration = 0.0, - reason = exceptionType, - success = false, - ) - } - - fun sendServiceInvocationEvent( - requestId: String, - requestContext: RequestContext, - responseContext: ResponseContext, - lastRecommendationIndex: Int, - invocationSuccess: Boolean, - latency: Double, - exceptionType: String?, - ) { - val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo - val (offset, line) = requestContext.caretPosition - - // since python now only supports UTG but not cross file context - val supContext = if (requestContext.fileContextInfo.programmingLanguage.isUTGSupported() && - requestContext.supplementalContext?.isUtg == true - ) { - requestContext.supplementalContext - } else if (requestContext.fileContextInfo.programmingLanguage.isSupplementalContextSupported() && - requestContext.supplementalContext?.isUtg == false - ) { - requestContext.supplementalContext - } else { - null - } - - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.serviceInvocation( - project = requestContext.project, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererCursorOffset = offset.toLong(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererLastSuggestionIndex = lastRecommendationIndex.toLong(), - codewhispererLineNumber = line.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerType, - duration = latency, - reason = exceptionType, - success = invocationSuccess, - credentialStartUrl = startUrl, - codewhispererImportRecommendationEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled(), - codewhispererSupplementalContextTimeout = supContext?.isProcessTimeout, - codewhispererSupplementalContextIsUtg = supContext?.isUtg, - codewhispererSupplementalContextLatency = supContext?.latency?.toDouble(), - codewhispererSupplementalContextLength = supContext?.contentLength?.toLong(), - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } - - fun sendUserDecisionEvent( - requestContext: RequestContext, - responseContext: ResponseContext, - detailContext: DetailContext, - index: Int, - suggestionState: CodewhispererSuggestionState, - numOfRecommendations: Int, - ) { - val requestId = detailContext.requestId - val recommendation = detailContext.recommendation - val (project, _, triggerTypeInfo) = requestContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val supplementalContext = requestContext.supplementalContext - - LOG.debug { - "Recording user decisions of recommendation. " + - "Index: $index, " + - "State: $suggestionState, " + - "Request ID: $requestId, " + - "Recommendation: ${recommendation.content()}" - } - val startUrl = getConnectionStartUrl(requestContext.connection) - val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled() - CodewhispererTelemetry.userDecision( - project = project, - codewhispererCompletionType = detailContext.completionType, - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererPaginationProgress = numOfRecommendations.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererSuggestionIndex = index.toLong(), - codewhispererSuggestionReferenceCount = recommendation.references().size.toLong(), - codewhispererSuggestionReferences = jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList()), - codewhispererSuggestionImportCount = if (importEnabled) recommendation.mostRelevantMissingImports().size.toLong() else null, - codewhispererSuggestionState = suggestionState, - codewhispererTriggerType = triggerTypeInfo.triggerType, - credentialStartUrl = startUrl, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - ) } fun sendUserTriggerDecisionEvent( - requestContext: RequestContext, - responseContext: ResponseContext, + project: Project, + latencyContext: LatencyContext, + sessionId: String, recommendationContext: RecommendationContext, - suggestionState: CodewhispererSuggestionState, - popupShownTime: Duration?, - suggestionReferenceCount: Int, - generatedLineCount: Int, - acceptedCharCount: Int, ) { - val project = requestContext.project - val totalImportCount = recommendationContext.details.fold(0) { grandTotal, detail -> - grandTotal + detail.recommendation.mostRelevantMissingImports().size - } - - val automatedTriggerType = requestContext.triggerTypeInfo.automatedTriggerType - val triggerChar = if (automatedTriggerType is CodeWhispererAutomatedTriggerType.SpecialChar) { - automatedTriggerType.specialChar.toString() - } else { - null - } - - val language = requestContext.fileContextInfo.programmingLanguage - - val classifierResult = requestContext.triggerTypeInfo.automatedTriggerType.calculationResult - - val classifierThreshold = CodeWhispererAutoTriggerService.getThreshold() - - val supplementalContext = requestContext.supplementalContext - val completionType = if (recommendationContext.details.isEmpty()) CodewhispererCompletionType.Line else recommendationContext.details[0].completionType - - // only send if it's a pro tier user - projectCoroutineScope(project).launch { - runIfIdcConnectionOrTelemetryEnabled(project) { - try { - val response = CodeWhispererClientAdaptor.getInstance(project) - .sendUserTriggerDecisionTelemetry( - requestContext, - responseContext, - completionType, - suggestionState, - suggestionReferenceCount, - generatedLineCount, - recommendationContext.details.size, - acceptedCharCount - ) - LOG.debug { - "Successfully sent user trigger decision telemetry. RequestId: ${response.responseMetadata().requestId()}" - } - } catch (e: Exception) { - val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null - LOG.debug { - "Failed to send user trigger decision telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" - } - } - } + AmazonQLspService.executeIfRunning(project) { server -> + val params = LogInlineCompletionSessionResultsParams( + sessionId = sessionId, + completionSessionResult = recommendationContext.details.associate { + it.itemId to InlineCompletionStates( + seen = it.hasSeen, + accepted = it.isAccepted, + discarded = it.isDiscarded + ) + }, + firstCompletionDisplayLatency = latencyContext.perceivedLatency, + totalSessionDisplayTime = CodeWhispererInvocationStatus.getInstance().completionShownTime?.let { Duration.between(it, Instant.now()) } + ?.toMillis()?.toDouble(), + typeaheadLength = recommendationContext.userInput.length.toLong() + ) + server.logInlineCompletionSessionResults(params) } - - CodewhispererTelemetry.userTriggerDecision( - project = project, - codewhispererSessionId = responseContext.sessionId, - codewhispererFirstRequestId = requestContext.latencyContext.firstRequestId, - credentialStartUrl = getConnectionStartUrl(requestContext.connection), - codewhispererIsPartialAcceptance = null, - codewhispererPartialAcceptanceCount = null, - codewhispererCharactersAccepted = acceptedCharCount.toLong(), - codewhispererCharactersRecommended = null, - codewhispererCompletionType = completionType, - codewhispererLanguage = language.toTelemetryType(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererLineNumber = requestContext.caretPosition.line.toLong(), - codewhispererCursorOffset = requestContext.caretPosition.offset.toLong(), - codewhispererSuggestionCount = recommendationContext.details.size.toLong(), - codewhispererSuggestionImportCount = totalImportCount.toLong(), - codewhispererTotalShownTime = popupShownTime?.toMillis()?.toDouble(), - codewhispererTriggerCharacter = triggerChar, - codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(), - codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(), - codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision, - codewhispererTimeToFirstRecommendation = requestContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreviousSuggestionState = previousUserTriggerDecision, - codewhispererSuggestionState = suggestionState, - codewhispererClassifierResult = classifierResult, - codewhispererClassifierThreshold = classifierThreshold, - codewhispererCustomizationArn = requestContext.customizationArn, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - codewhispererSupplementalContextStrategyId = supplementalContext?.strategy.toString(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererFeatureEvaluations = CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry() - ) } private fun mapToTelemetryScope(codeAnalysisScope: CodeWhispererConstants.CodeAnalysisScope, initiatedByChat: Boolean): CodewhispererCodeScanScope = @@ -421,210 +200,10 @@ class CodeWhispererTelemetryService { ) } - fun enqueueAcceptedSuggestionEntry( - requestId: String, - requestContext: RequestContext, - responseContext: ResponseContext, - time: Instant, - vFile: VirtualFile?, - range: RangeMarker, - suggestion: String, - selectedIndex: Int, - completionType: CodewhispererCompletionType, - ) { - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage - CodeWhispererUserModificationTracker.getInstance(requestContext.project).enqueue( - AcceptedSuggestionEntry( - time, - vFile, - range, - suggestion, - responseContext.sessionId, - requestId, - selectedIndex, - requestContext.triggerTypeInfo.triggerType, - completionType, - codewhispererLanguage, - null, - null, - requestContext.connection - ) - ) - } - - fun sendUserDecisionEventForAll( - requestContext: RequestContext, - responseContext: ResponseContext, - recommendationContext: RecommendationContext, - sessionContext: SessionContext, - hasUserAccepted: Boolean, - popupShownTime: Duration? = null, - ) { - val detailContexts = recommendationContext.details - val decisions = mutableListOf() - - detailContexts.forEachIndexed { index, detailContext -> - val suggestionState = recordSuggestionState( - index, - sessionContext.selectedIndex, - sessionContext.seen.contains(index), - hasUserAccepted, - detailContext.isDiscarded, - detailContext.recommendation.content().isEmpty() - ) - sendUserDecisionEvent(requestContext, responseContext, detailContext, index, suggestionState, detailContexts.size) - - decisions.add(suggestionState) - } - - with(aggregateUserDecision(decisions)) { - // the order of the following matters - // step 1, send out current decision - previousUserTriggerDecisionTimestamp = Instant.now() - - val referenceCount = if (hasUserAccepted && detailContexts[sessionContext.selectedIndex].recommendation.hasReferences()) 1 else 0 - val acceptedContent = - if (hasUserAccepted) { - detailContexts[sessionContext.selectedIndex].recommendation.content() - } else { - "" - } - val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size - val acceptedCharCount = acceptedContent.length - sendUserTriggerDecisionEvent( - requestContext, - responseContext, - recommendationContext, - CodewhispererSuggestionState.from(this.toString()), - popupShownTime, - referenceCount, - generatedLineCount, - acceptedCharCount - ) - - // step 2, put current decision into queue for later reference - previousUserTriggerDecisions.add(this) - // we need this as well because AutotriggerService will reset the queue periodically - CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(this) - } - } - - /** - * Aggregate recommendation level user decision to trigger level user decision based on the following rule - * - Accept if there is an Accept - * - Reject if there is a Reject - * - Empty if all decisions are Empty - * - Record the accepted suggestion index - * - Discard otherwise - */ - fun aggregateUserDecision(decisions: List): CodewhispererPreviousSuggestionState { - var isEmpty = true - - for (decision in decisions) { - if (decision == CodewhispererSuggestionState.Accept) { - return CodewhispererPreviousSuggestionState.Accept - } else if (decision == CodewhispererSuggestionState.Reject) { - return CodewhispererPreviousSuggestionState.Reject - } else if (decision != CodewhispererSuggestionState.Empty) { - isEmpty = false - } - } - - return if (isEmpty) { - CodewhispererPreviousSuggestionState.Empty - } else { - CodewhispererPreviousSuggestionState.Discard - } - } - - fun sendPerceivedLatencyEvent( - requestId: String, - requestContext: RequestContext, - responseContext: ResponseContext, - latency: Double, - ) { - val (project, _, triggerTypeInfo) = requestContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.perceivedLatency( - project = project, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerTypeInfo.triggerType, - duration = latency, - passive = true, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } - - fun sendClientComponentLatencyEvent(states: InvocationContext) { - val requestContext = states.requestContext - val responseContext = states.responseContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.clientComponentLatency( - project = requestContext.project, - codewhispererSessionId = responseContext.sessionId, - codewhispererRequestId = requestContext.latencyContext.firstRequestId, - codewhispererFirstCompletionLatency = requestContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreprocessingLatency = requestContext.latencyContext.getCodeWhispererPreprocessingLatency(), - codewhispererEndToEndLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency(), - codewhispererAllCompletionsLatency = requestContext.latencyContext.getCodeWhispererAllCompletionsLatency(), - codewhispererPostprocessingLatency = requestContext.latencyContext.getCodeWhispererPostprocessingLatency(), - codewhispererCredentialFetchingLatency = requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } - fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) { // Project instance is not needed. We look at these metrics for each clientId. CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType) } - - fun recordSuggestionState( - index: Int, - selectedIndex: Int, - hasSeen: Boolean, - hasUserAccepted: Boolean, - isDiscarded: Boolean, - isEmpty: Boolean, - ): CodewhispererSuggestionState = - if (isEmpty) { - CodewhispererSuggestionState.Empty - } else if (isDiscarded) { - CodewhispererSuggestionState.Discard - } else if (!hasSeen) { - CodewhispererSuggestionState.Unseen - } else if (hasUserAccepted) { - if (selectedIndex == index) { - CodewhispererSuggestionState.Accept - } else { - CodewhispererSuggestionState.Ignore - } - } else { - CodewhispererSuggestionState.Reject - } - - @TestOnly - fun previousDecisions(): Queue { - assert(ApplicationManager.getApplication().isUnitTestMode) - return this.previousUserTriggerDecisions - } - - fun sendInvalidZipEvent(filePath: Path, projectRoot: Path, relativePath: String) { - CodewhispererTelemetry.invalidZip( - filePath = filePath.pathString, - workspaceRoot = projectRoot.pathString, - relativePath = relativePath - ) - } } fun isTelemetryEnabled(): Boolean = AwsSettings.getInstance().isTelemetryEnabled diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt index 2125f79ad40..73106cd4714 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt @@ -3,283 +3,37 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.editor.RangeMarker import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import kotlinx.coroutines.launch -import org.apache.commons.collections4.queue.CircularFifoQueue -import org.jetbrains.annotations.TestOnly -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -import software.amazon.awssdk.services.codewhispererruntime.model.Completion import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope -import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.InlineCompletionStates +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LogInlineCompletionSessionResultsParams import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanTelemetryEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType +import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getGettingStartedTaskType -import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.telemetry.CodewhispererCodeScanScope -import software.aws.toolkits.telemetry.CodewhispererCompletionType import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask -import software.aws.toolkits.telemetry.CodewhispererLanguage -import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState -import software.aws.toolkits.telemetry.CodewhispererSuggestionState import software.aws.toolkits.telemetry.CodewhispererTelemetry -import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Component import software.aws.toolkits.telemetry.Result import java.time.Duration import java.time.Instant -import java.util.Queue @Service class CodeWhispererTelemetryServiceNew { - // store previous 5 userTrigger decisions - private val previousUserTriggerDecisions = CircularFifoQueue(5) - - private var previousUserTriggerDecisionTimestamp: Instant? = null - - private val codewhispererTimeSinceLastUserDecision: Double? = - previousUserTriggerDecisionTimestamp?.let { - Duration.between(it, Instant.now()).toMillis().toDouble() - } - - val previousUserTriggerDecision: CodewhispererPreviousSuggestionState? - get() = if (previousUserTriggerDecisions.isNotEmpty()) previousUserTriggerDecisions.last() else null companion object { fun getInstance(): CodeWhispererTelemetryServiceNew = service() val LOG = getLogger() - const val NO_ACCEPTED_INDEX = -1 - } - - fun sendFailedServiceInvocationEvent(project: Project, exceptionType: String?) { - CodewhispererTelemetry.serviceInvocation( - project = project, - codewhispererCursorOffset = 0, - codewhispererLanguage = CodewhispererLanguage.Unknown, - codewhispererLastSuggestionIndex = -1, - codewhispererLineNumber = 0, - codewhispererTriggerType = CodewhispererTriggerType.Unknown, - duration = 0.0, - reason = exceptionType, - success = false, - ) - } - - fun sendServiceInvocationEvent( - jobId: Int, - requestId: String, - requestContext: RequestContextNew, - responseContext: ResponseContext, - lastRecommendationIndex: Int, - invocationSuccess: Boolean, - latency: Double, - exceptionType: String?, - ) { - LOG.debug { "Sending serviceInvocation for $requestId, jobId: $jobId" } - val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo - val (offset, line) = requestContext.caretPosition - - // since python now only supports UTG but not cross file context - val supContext = if (requestContext.fileContextInfo.programmingLanguage.isUTGSupported() && - requestContext.supplementalContext?.isUtg == true - ) { - requestContext.supplementalContext - } else if (requestContext.fileContextInfo.programmingLanguage.isSupplementalContextSupported() && - requestContext.supplementalContext?.isUtg == false - ) { - requestContext.supplementalContext - } else { - null - } - - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.serviceInvocation( - project = requestContext.project, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererCursorOffset = offset.toLong(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererLastSuggestionIndex = lastRecommendationIndex.toLong(), - codewhispererLineNumber = line.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerType, - duration = latency, - reason = exceptionType, - success = invocationSuccess, - credentialStartUrl = startUrl, - codewhispererImportRecommendationEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled(), - codewhispererSupplementalContextTimeout = supContext?.isProcessTimeout, - codewhispererSupplementalContextIsUtg = supContext?.isUtg, - codewhispererSupplementalContextLatency = supContext?.latency?.toDouble(), - codewhispererSupplementalContextLength = supContext?.contentLength?.toLong(), - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } - - fun sendUserDecisionEvent( - requestContext: RequestContextNew, - responseContext: ResponseContext, - detailContext: DetailContextNew, - index: Int, - suggestionState: CodewhispererSuggestionState, - numOfRecommendations: Int, - ) { - val requestId = detailContext.requestId - val recommendation = detailContext.recommendation - val (project, _, triggerTypeInfo) = requestContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val supplementalContext = requestContext.supplementalContext - - LOG.debug { - "Recording user decisions of recommendation. " + - "Index: $index, " + - "State: $suggestionState, " + - "Request ID: $requestId, " + - "Recommendation: ${recommendation.content()}" - } - val startUrl = getConnectionStartUrl(requestContext.connection) - val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled() - CodewhispererTelemetry.userDecision( - project = project, - codewhispererCompletionType = detailContext.completionType, - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererPaginationProgress = numOfRecommendations.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererSuggestionIndex = index.toLong(), - codewhispererSuggestionReferenceCount = recommendation.references().size.toLong(), - codewhispererSuggestionReferences = jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList()), - codewhispererSuggestionImportCount = if (importEnabled) recommendation.mostRelevantMissingImports().size.toLong() else null, - codewhispererSuggestionState = suggestionState, - codewhispererTriggerType = triggerTypeInfo.triggerType, - credentialStartUrl = startUrl, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - ) - } - - fun sendUserTriggerDecisionEvent( - sessionContext: SessionContextNew, - requestContext: RequestContextNew, - responseContext: ResponseContext, - recommendationContext: RecommendationContextNew, - suggestionState: CodewhispererSuggestionState, - popupShownTime: Duration?, - suggestionReferenceCount: Int, - generatedLineCount: Int, - acceptedCharCount: Int, - ) { - val project = requestContext.project - val totalImportCount = recommendationContext.details.fold(0) { grandTotal, detail -> - grandTotal + detail.recommendation.mostRelevantMissingImports().size - } - - val automatedTriggerType = requestContext.triggerTypeInfo.automatedTriggerType - val triggerChar = if (automatedTriggerType is CodeWhispererAutomatedTriggerType.SpecialChar) { - automatedTriggerType.specialChar.toString() - } else { - null - } - - val language = requestContext.fileContextInfo.programmingLanguage - - val classifierResult = requestContext.triggerTypeInfo.automatedTriggerType.calculationResult - - val classifierThreshold = CodeWhispererAutoTriggerService.getThreshold() - - val supplementalContext = requestContext.supplementalContext - val completionType = if (recommendationContext.details.isEmpty()) CodewhispererCompletionType.Line else recommendationContext.details[0].completionType - - // only send if it's a pro tier user - projectCoroutineScope(project).launch { - runIfIdcConnectionOrTelemetryEnabled(project) { - try { - val response = CodeWhispererClientAdaptor.getInstance(project) - .sendUserTriggerDecisionTelemetry( - sessionContext, - requestContext, - responseContext, - completionType, - suggestionState, - suggestionReferenceCount, - generatedLineCount, - recommendationContext.details.size, - acceptedCharCount - ) - LOG.debug { - "Successfully sent user trigger decision telemetry. RequestId: ${response.responseMetadata().requestId()}" - } - } catch (e: Exception) { - val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null - LOG.debug { - "Failed to send user trigger decision telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" - } - } - } - } - - CodewhispererTelemetry.userTriggerDecision( - project = project, - codewhispererSessionId = responseContext.sessionId, - codewhispererFirstRequestId = sessionContext.latencyContext.firstRequestId, - credentialStartUrl = getConnectionStartUrl(requestContext.connection), - codewhispererIsPartialAcceptance = null, - codewhispererPartialAcceptanceCount = null, - codewhispererCharactersAccepted = acceptedCharCount.toLong(), - codewhispererCharactersRecommended = null, - codewhispererCompletionType = completionType, - codewhispererLanguage = language.toTelemetryType(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererLineNumber = requestContext.caretPosition.line.toLong(), - codewhispererCursorOffset = requestContext.caretPosition.offset.toLong(), - codewhispererSuggestionCount = recommendationContext.details.size.toLong(), - codewhispererSuggestionImportCount = totalImportCount.toLong(), - codewhispererTotalShownTime = popupShownTime?.toMillis()?.toDouble(), - codewhispererTriggerCharacter = triggerChar, - codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(), - codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(), - codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision, - codewhispererTimeToFirstRecommendation = sessionContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreviousSuggestionState = previousUserTriggerDecision, - codewhispererSuggestionState = suggestionState, - codewhispererClassifierResult = classifierResult, - codewhispererClassifierThreshold = classifierThreshold, - codewhispererCustomizationArn = requestContext.customizationArn, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - codewhispererSupplementalContextStrategyId = supplementalContext?.strategy.toString(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererFeatureEvaluations = CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry() - ) } fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent, project: Project? = null) { @@ -354,165 +108,31 @@ class CodeWhispererTelemetryServiceNew { ) } - fun enqueueAcceptedSuggestionEntry( - requestId: String, - requestContext: RequestContextNew, - responseContext: ResponseContext, - time: Instant, - vFile: VirtualFile?, - range: RangeMarker, - suggestion: String, - selectedIndex: Int, - completionType: CodewhispererCompletionType, - ) { - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage - CodeWhispererUserModificationTracker.getInstance(requestContext.project).enqueue( - AcceptedSuggestionEntry( - time, - vFile, - range, - suggestion, - responseContext.sessionId, - requestId, - selectedIndex, - requestContext.triggerTypeInfo.triggerType, - completionType, - codewhispererLanguage, - null, - null, - requestContext.connection - ) - ) - } - - fun sendUserDecisionEventForAll( - sessionContext: SessionContextNew, - hasUserAccepted: Boolean, - popupShownTime: Duration? = null, - ) { - CodeWhispererServiceNew.getInstance().getAllPaginationSessions().forEach { (jobId, state) -> - if (state == null) return@forEach - val details = state.recommendationContext.details - - val decisions = details.mapIndexed { index, detail -> - val suggestionState = recordSuggestionState(detail, hasUserAccepted) - sendUserDecisionEvent(state.requestContext, state.responseContext, detail, index, suggestionState, details.size) - - suggestionState - } - LOG.debug { "jobId: $jobId, userDecisions: [${decisions.joinToString(", ")}]" } - - with(aggregateUserDecision(decisions)) { - // the order of the following matters - // step 1, send out current decision - LOG.debug { "jobId: $jobId, userTriggerDecision: $this" } - previousUserTriggerDecisionTimestamp = Instant.now() - - val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo() - val recommendation = - if (hasUserAccepted) { - previews[sessionContext.selectedIndex].detail.recommendation - } else { - Completion.builder().content("").references(emptyList()).build() - } - val referenceCount = if (hasUserAccepted && recommendation.hasReferences()) 1 else 0 - val acceptedContent = recommendation.content() - val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size - val acceptedCharCount = acceptedContent.length - sendUserTriggerDecisionEvent( - sessionContext, - state.requestContext, - state.responseContext, - state.recommendationContext, - this, - popupShownTime, - referenceCount, - generatedLineCount, - acceptedCharCount + fun sendUserTriggerDecisionEvent(project: Project, latencyContext: LatencyContext) { + AmazonQLspService.executeIfRunning(project) { server -> + CodeWhispererServiceNew.getInstance().getAllPaginationSessions().forEach { jobId, state -> + if (state == null) return@forEach + val params = LogInlineCompletionSessionResultsParams( + sessionId = state.responseContext.sessionId, + completionSessionResult = state.recommendationContext.details.associate { + it.itemId to InlineCompletionStates( + seen = it.hasSeen, + accepted = it.isAccepted, + discarded = it.isDiscarded + ) + }, + firstCompletionDisplayLatency = latencyContext.perceivedLatency, + totalSessionDisplayTime = CodeWhispererInvocationStatus.getInstance().completionShownTime?.let { Duration.between(it, Instant.now()) } + ?.toMillis()?.toDouble(), + typeaheadLength = state.recommendationContext.userInput.length.toLong() ) - - // step 2, put current decision into queue for later reference - if (this != CodewhispererSuggestionState.Ignore && this != CodewhispererSuggestionState.Unseen) { - val previousState = CodewhispererPreviousSuggestionState.from(this.toString()) - // we need this as well because AutoTriggerService will reset the queue periodically - previousUserTriggerDecisions.add(previousState) - CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(previousState) - } + server.logInlineCompletionSessionResults(params) } } } - /** - * Aggregate recommendation level user decision to trigger level user decision based on the following rule - * - Accept if there is an Accept - * - Reject if there is a Reject - * - Empty if all decisions are Empty - * - Ignore if at least one suggestion is seen and there's an accept for another trigger in the same display session - * - Unseen if the whole trigger is not seen (but has valid suggestions) - * - Record the accepted suggestion index - * - Discard otherwise - */ - fun aggregateUserDecision(decisions: List): CodewhispererSuggestionState { - var isEmpty = true - var isUnseen = true - var isDiscard = true - - for (decision in decisions) { - if (decision == CodewhispererSuggestionState.Accept) { - return CodewhispererSuggestionState.Accept - } else if (decision == CodewhispererSuggestionState.Reject) { - return CodewhispererSuggestionState.Reject - } else if (decision == CodewhispererSuggestionState.Unseen) { - isEmpty = false - isDiscard = false - } else if (decision == CodewhispererSuggestionState.Ignore) { - isUnseen = false - isEmpty = false - isDiscard = false - } else if (decision == CodewhispererSuggestionState.Discard) { - isEmpty = false - } - } - - return if (isEmpty) { - CodewhispererSuggestionState.Empty - } else if (isDiscard) { - CodewhispererSuggestionState.Discard - } else if (isUnseen) { - CodewhispererSuggestionState.Unseen - } else { - CodewhispererSuggestionState.Ignore - } - } - fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) { // Project instance is not needed. We look at these metrics for each clientId. CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType) } - - fun recordSuggestionState( - detail: DetailContextNew, - hasUserAccepted: Boolean, - ): CodewhispererSuggestionState = - if (detail.recommendation.content().isEmpty()) { - CodewhispererSuggestionState.Empty - } else if (detail.isDiscarded) { - CodewhispererSuggestionState.Discard - } else if (!detail.hasSeen) { - CodewhispererSuggestionState.Unseen - } else if (hasUserAccepted) { - if (detail.isAccepted) { - CodewhispererSuggestionState.Accept - } else { - CodewhispererSuggestionState.Ignore - } - } else { - CodewhispererSuggestionState.Reject - } - - @TestOnly - fun previousDecisions(): Queue { - assert(ApplicationManager.getApplication().isUnitTestMode) - return this.previousUserTriggerDecisions - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererUserModificationTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererUserModificationTracker.kt deleted file mode 100644 index 2df4d73db5a..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererUserModificationTracker.kt +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.Alarm -import com.intellij.util.AlarmFactory -import info.debatty.java.stringsimilarity.Levenshtein -import org.assertj.core.util.VisibleForTesting -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -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.credentials.ToolkitConnection -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.language.CodeWhispererLanguageManager -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getUnmodifiedAcceptedCharsCount -import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.InsertedCodeModificationEntry -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl -import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings -import software.aws.toolkits.telemetry.AmazonqTelemetry -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererRuntime -import software.aws.toolkits.telemetry.CodewhispererTelemetry -import software.aws.toolkits.telemetry.CodewhispererTriggerType -import java.time.Duration -import java.time.Instant -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.min - -data class AcceptedSuggestionEntry( - override val time: Instant, - val vFile: VirtualFile?, - val range: RangeMarker, - val suggestion: String, - val sessionId: String, - val requestId: String, - val index: Int, - val triggerType: CodewhispererTriggerType, - val completionType: CodewhispererCompletionType, - val codewhispererLanguage: CodeWhispererProgrammingLanguage, - val codewhispererRuntime: CodewhispererRuntime?, - val codewhispererRuntimeSource: String?, - val connection: ToolkitConnection?, -) : UserModificationTrackingEntry - -data class CodeInsertionDiff( - val original: String, - val modified: String, - val diff: Double, -) - -fun CodeInsertionDiff?.percentage(): Double = when { - this == null -> 1.0 - - // TODO: should revisit this case - original.isEmpty() || modified.isEmpty() -> 1.0 - - else -> min(1.0, (diff / original.length)) -} - -@Service(Service.Level.PROJECT) -class CodeWhispererUserModificationTracker(private val project: Project) : Disposable { - private val acceptedSuggestions = LinkedBlockingDeque(DEFAULT_MAX_QUEUE_SIZE) - private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this) - - private val isShuttingDown = AtomicBoolean(false) - - init { - scheduleCodeWhispererTracker() - } - - private fun scheduleCodeWhispererTracker() { - if (!alarm.isDisposed && !isShuttingDown.get()) { - alarm.addRequest({ flush() }, DEFAULT_CHECK_INTERVAL.toMillis()) - } - } - - private fun isTelemetryEnabled(): Boolean = AwsSettings.getInstance().isTelemetryEnabled - - fun enqueue(event: UserModificationTrackingEntry) { - if (!isTelemetryEnabled()) { - return - } - - acceptedSuggestions.add(event) - LOG.debug { "Enqueue Accepted Suggestion on line $event.lineNumber in $event.filePath" } - } - - private fun flush() { - try { - if (!isTelemetryEnabled()) { - acceptedSuggestions.clear() - return - } - - val copyList = LinkedBlockingDeque() - - val currentTime = Instant.now() - for (acceptedSuggestion in acceptedSuggestions) { - if (Duration.between(acceptedSuggestion.time, currentTime).seconds > DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS) { - LOG.debug { "Passed $DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS for $acceptedSuggestion" } - when (acceptedSuggestion) { - is AcceptedSuggestionEntry -> emitTelemetryOnSuggestion(acceptedSuggestion) - is InsertedCodeModificationEntry -> emitTelemetryOnChatCodeInsert(acceptedSuggestion) - else -> {} - } - } else { - copyList.add(acceptedSuggestion) - } - } - - acceptedSuggestions.clear() - acceptedSuggestions.addAll(copyList) - } finally { - scheduleCodeWhispererTracker() - } - } - - private fun emitTelemetryOnChatCodeInsert(insertedCode: InsertedCodeModificationEntry) { - val modificationPercentage = try { - val file = insertedCode.vFile - if (file == null || (!file.isValid)) throw Exception("Record OnChatCodeInsert - invalid file") - - val document = runReadAction { - FileDocumentManager.getInstance().getDocument(file) - } - val currentString = document?.getText( - TextRange(insertedCode.range.startOffset, insertedCode.range.endOffset) - ) - checkDiff(currentString?.trim(), insertedCode.originalString.trim()) - } catch (e: Exception) { - null - } - - sendModificationWithChatTelemetry(insertedCode, modificationPercentage) - } - - private fun emitTelemetryOnSuggestion(acceptedSuggestion: AcceptedSuggestionEntry) { - val file = acceptedSuggestion.vFile - - if (file == null || (!file.isValid) || !acceptedSuggestion.range.isValid) { - sendModificationTelemetry(acceptedSuggestion, null) - sendUserModificationTelemetryToServiceAPI(acceptedSuggestion) - } else { - // Will remove this later when we truly don't need toolkit user modification telemetry anymore - val document = runReadAction { - FileDocumentManager.getInstance().getDocument(file) - } - val start = acceptedSuggestion.range.startOffset - val end = acceptedSuggestion.range.endOffset - if (document != null) { - if (start < 0 || end < start || end > document.textLength) { - LOG.warn { - "Invalid range for suggestion ${acceptedSuggestion.requestId}: " + - "start=$start, end=$end, docLength=${document.textLength}" - } - sendModificationTelemetry(acceptedSuggestion, null) - sendUserModificationTelemetryToServiceAPI(acceptedSuggestion) - return - } - } - - val currentString = document?.getText( - TextRange(acceptedSuggestion.range.startOffset, acceptedSuggestion.range.endOffset) - ) - val modificationPercentage = checkDiff(currentString?.trim(), acceptedSuggestion.suggestion.trim()) - sendModificationTelemetry(acceptedSuggestion, modificationPercentage) - sendUserModificationTelemetryToServiceAPI(acceptedSuggestion) - } - } - - /** - * Use Levenshtein distance to check how - * Levenshtein distance was preferred over Jaro–Winkler distance for simplicity - */ - @VisibleForTesting - internal fun checkDiff(currString: String?, acceptedString: String?): CodeInsertionDiff? { - if (currString == null || acceptedString == null || acceptedString.isEmpty() || currString.isEmpty()) { - return null - } - val diff = checker.distance(currString, acceptedString) - return CodeInsertionDiff( - original = acceptedString, - modified = currString, - diff = diff - ) - } - - private fun sendModificationTelemetry(suggestion: AcceptedSuggestionEntry, diff: CodeInsertionDiff?) { - LOG.debug { "Sending user modification telemetry. Request Id: ${suggestion.requestId}" } - val startUrl = getConnectionStartUrl(suggestion.connection) - CodewhispererTelemetry.userModification( - project = project, - codewhispererCompletionType = suggestion.completionType, - codewhispererLanguage = suggestion.codewhispererLanguage.toTelemetryType(), - codewhispererModificationPercentage = diff.percentage(), - codewhispererRequestId = suggestion.requestId, - codewhispererRuntime = suggestion.codewhispererRuntime, - codewhispererRuntimeSource = suggestion.codewhispererRuntimeSource, - codewhispererSessionId = suggestion.sessionId, - codewhispererSuggestionIndex = suggestion.index.toLong(), - codewhispererTriggerType = suggestion.triggerType, - credentialStartUrl = startUrl, - codewhispererCharactersModified = diff?.modified?.length?.toLong() ?: 0, - codewhispererCharactersAccepted = diff?.original?.length?.toLong() ?: 0 - ) - } - - private fun sendModificationWithChatTelemetry(insertedCode: InsertedCodeModificationEntry, diff: CodeInsertionDiff?) { - AmazonqTelemetry.modifyCode( - cwsprChatConversationId = insertedCode.conversationId, - cwsprChatMessageId = insertedCode.messageId, - cwsprChatModificationPercentage = diff.percentage(), - credentialStartUrl = getStartUrl(project) - ) - val lang = insertedCode.vFile?.programmingLanguage() ?: CodeWhispererUnknownLanguage.INSTANCE - - CodeWhispererClientAdaptor.getInstance( - project - ).sendChatUserModificationTelemetry( - insertedCode.conversationId, - insertedCode.messageId, - lang, - diff.percentage(), - CodeWhispererSettings.getInstance().isProjectContextEnabled(), - CodeWhispererModelConfigurator.getInstance().activeCustomization(project) - ).also { - LOG.debug { "Successfully sendTelemetryEvent for ChatModificationWithChat with requestId: ${it.responseMetadata().requestId()}" } - } - } - - private fun sendUserModificationTelemetryToServiceAPI( - suggestion: AcceptedSuggestionEntry, - ) { - runIfIdcConnectionOrTelemetryEnabled(project) { - try { - // should be impossible from the caller logic - if (suggestion.vFile == null) return@runIfIdcConnectionOrTelemetryEnabled - val document = runReadAction { - FileDocumentManager.getInstance().getDocument(suggestion.vFile) - } - val modifiedSuggestion = document?.getText( - TextRange(suggestion.range.startOffset, suggestion.range.endOffset) - ).orEmpty() - val response = CodeWhispererClientAdaptor.getInstance(project) - .sendUserModificationTelemetry( - suggestion.sessionId, - suggestion.requestId, - CodeWhispererLanguageManager.getInstance().getLanguage(suggestion.vFile), - CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn, - suggestion.suggestion.length, - getUnmodifiedAcceptedCharsCount(suggestion.suggestion, modifiedSuggestion) - ) - LOG.debug { "Successfully sent user modification telemetry. RequestId: ${response.responseMetadata().requestId()}" } - } catch (e: Exception) { - val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null - LOG.debug { - "Failed to send user modification telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" - } - } - } - } - - companion object { - private val DEFAULT_CHECK_INTERVAL = Duration.ofMinutes(1) - private const val DEFAULT_MAX_QUEUE_SIZE = 10000 - private const val DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS = 300 // 5 minutes - - private val checker = Levenshtein() - - private val LOG = getLogger() - - fun getInstance(project: Project) = project.service() - } - - override fun dispose() { - if (isShuttingDown.getAndSet(true)) { - return - } - - flush() - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt deleted file mode 100644 index 5bcdd289c03..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiDocumentManager -import com.intellij.util.Alarm -import com.intellij.util.AlarmFactory -import com.intellij.util.concurrency.annotations.RequiresReadLock -import com.intellij.util.messages.MessageBusConnection -import com.intellij.util.messages.Topic -import org.jetbrains.annotations.TestOnly -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -import software.aws.toolkits.core.utils.debug -import software.aws.toolkits.core.utils.getLogger -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.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker.Companion.Q_FEATURE_TOPIC -import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled -import software.aws.toolkits.jetbrains.settings.AwsSettings -import java.time.Duration -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger - -@Service(Service.Level.PROJECT) -class UserWrittenCodeTracker(private val project: Project) : Disposable { - val userWrittenCodeLineCount = mutableMapOf() - val userWrittenCodeCharacterCount = mutableMapOf() - private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this) - - private val isShuttingDown = AtomicBoolean(false) - val qInvocationCount: AtomicInteger = AtomicInteger(0) - private val isQMakingEdits = AtomicBoolean(false) - private val isActive: AtomicBoolean = AtomicBoolean(false) - private var conn: MessageBusConnection? = null - - @Synchronized - fun activateTrackerIfNotActive() { - // tracker will only be activated if and only if IsTelemetryEnabled = true && isActive = false - if (!isTelemetryEnabled() || isActive.get()) return - isActive.set(true) - // count q service invocations - conn = ApplicationManager.getApplication().messageBus.connect() - conn?.subscribe( - Q_FEATURE_TOPIC, - object : QFeatureListener { - override fun onEvent(event: QFeatureEvent) { - when (event) { - QFeatureEvent.INVOCATION -> qInvocationCount.getAndIncrement() - QFeatureEvent.STARTS_EDITING -> isQMakingEdits.set(true) - QFeatureEvent.FINISHES_EDITING -> isQMakingEdits.set(false) - } - } - } - ) - scheduleTracker() - } - - fun reset() { - userWrittenCodeLineCount.clear() - userWrittenCodeCharacterCount.clear() - qInvocationCount.set(0) - isQMakingEdits.set(false) - isActive.set(false) - isShuttingDown.set(false) - } - - private fun isTelemetryEnabled(): Boolean = AwsSettings.getInstance().isTelemetryEnabled - - private fun scheduleTracker() { - if (!alarm.isDisposed && !isShuttingDown.get()) { - alarm.addRequest({ flush() }, Duration.ofSeconds(DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS).toMillis()) - } - } - - private fun flush() { - try { - if (!isTelemetryEnabled() || qInvocationCount.get() <= 0) { - return - } - emitCodeWhispererCodeContribution() - } finally { - reset() - scheduleTracker() - } - } - - @RequiresReadLock - internal fun documentChanged(event: DocumentEvent) { - // do not listen to document changed made by Amazon Q itself - if (isQMakingEdits.get() || !isActive.get()) { - return - } - - // When open a file for the first time, IDE will also emit DocumentEvent for loading with `isWholeTextReplaced = true` - // Added this condition to filter out those events - if (event.isWholeTextReplaced) { - LOG.debug { "event with isWholeTextReplaced flag: $event" } - if (event.oldTimeStamp == 0L) return - } - // only count total tokens when it is a user keystroke input - // do not count doc changes from copy & paste of >=50 characters - // do not count other changes from formatter, git command, etc - // edge case: event can be from user hit enter with indentation where change is \n\t\t, count as 1 char increase in total chars - // when event is auto closing [{(', there will be 2 separated events, both count as 1 char increase in total chars - val text = event.newFragment.toString() - val lines = text.split('\n').size - 1 - if (event.newLength < COPY_THRESHOLD && !isIntelliJMultiSpacesInsert(text) && text.isNotEmpty()) { - // count doc changes from <50 multi character input as total user written code - // ignore all white space changes, this usually comes from IntelliJ formatting - val language = PsiDocumentManager.getInstance(project).getPsiFile(event.document)?.programmingLanguage() - if (language != null) { - userWrittenCodeLineCount[language] = userWrittenCodeLineCount.getOrDefault(language, 0) + lines - userWrittenCodeCharacterCount[language] = userWrittenCodeCharacterCount.getOrDefault(language, 0) + event.newLength - } - } - } - - // intelliJ sometimes insert multi spaces for indentation, this is not user written code - private fun isIntelliJMultiSpacesInsert(text: String) = text.trim { it == ' ' }.isEmpty() && text.length > 1 - - private fun emitCodeWhispererCodeContribution() { - val customizationArn: String? = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn - for ((language, _) in userWrittenCodeCharacterCount) { - if (userWrittenCodeCharacterCount.getOrDefault(language, 0) <= 0) { - continue - } - runIfIdcConnectionOrTelemetryEnabled(project) { - // here acceptedTokensSize is the count of accepted chars post user modification - try { - val response = CodeWhispererClientAdaptor.getInstance(project).sendCodePercentageTelemetry( - language, - customizationArn, - 0, - 0, - 0, - userWrittenCodeCharacterCount = userWrittenCodeCharacterCount.getOrDefault(language, 0), - userWrittenCodeLineCount = userWrittenCodeLineCount.getOrDefault(language, 0) - ) - LOG.debug { "Successfully sent code percentage telemetry. RequestId: ${response.responseMetadata().requestId()}" } - } catch (e: Exception) { - val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null - LOG.debug { - "Failed to send code percentage telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" - } - } - } - } - } - - companion object { - private const val DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS = 300L // 5 minutes - - private const val COPY_THRESHOLD = 50 - private val LOG = getLogger() - - fun getInstance(project: Project) = project.service() - - val Q_FEATURE_TOPIC: Topic = Topic.create( - "Q service events", - QFeatureListener::class.java - ) - } - - override fun dispose() { - if (isShuttingDown.getAndSet(true)) { - return - } - conn?.disconnect() - flush() - } - - @TestOnly - fun forceTrackerFlush() { - alarm.drainRequestsInTest() - } -} - -enum class QFeatureEvent { - INVOCATION, - STARTS_EDITING, - FINISHES_EDITING, -} - -interface QFeatureListener { - fun onEvent(event: QFeatureEvent) -} - -fun broadcastQEvent(event: QFeatureEvent) = - ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC) - .onEvent(event) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceComponents.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceComponents.kt index fb73fac67ee..f80a87053a2 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceComponents.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceComponents.kt @@ -8,9 +8,9 @@ import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.ui.components.ActionLink -import software.amazon.awssdk.services.codewhispererruntime.model.Reference import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue @@ -105,31 +105,31 @@ class CodeWhispererCodeReferenceComponents(private val project: Project) { text = message("codewhisperer.toolwindow.entry.suffix", path ?: "", choice, line) }.asCodeReferencePanelFont() - fun codeReferenceRecordPanel(ref: Reference, relativePath: String?, lineNums: String?) = JPanel(GridBagLayout()).apply { + fun codeReferenceRecordPanel(ref: InlineCompletionReference, relativePath: String?, lineNums: String?) = JPanel(GridBagLayout()).apply { background = EditorColorsManager.getInstance().globalScheme.defaultBackground border = BorderFactory.createEmptyBorder(5, 0, 0, 0) add(acceptRecommendationPrefixText, inlineLabelConstraints) // if url to source package/repo is missing, the UX remains the same as we have for now // if url to source package/repo is present, the url pointing to the source will be present and remove the hyperlink to SPDX - if (ref.url().isNullOrEmpty()) { + if (ref.referenceUrl.isEmpty()) { add( - licenseNameLink(ref.licenseName()).apply { + licenseNameLink(ref.licenseName).apply { font = font.deriveFont(Font.ITALIC + Font.BOLD) }, inlineLabelConstraints ) add(JLabel(" from ").asCodeReferencePanelFont(), inlineLabelConstraints) - add(JLabel(ref.repository()), inlineLabelConstraints) + add(JLabel(ref.referenceName), inlineLabelConstraints) } else { add( - JLabel(ref.licenseName()).apply { + JLabel(ref.licenseName).apply { font = font.deriveFont(Font.ITALIC + Font.BOLD) }, inlineLabelConstraints ) add(JLabel(" from ").asCodeReferencePanelFont(), inlineLabelConstraints) - add(repoNameLink(ref.repository(), ref.url()), inlineLabelConstraints) + add(repoNameLink(ref.referenceName, ref.referenceUrl), inlineLabelConstraints) } if (!lineNums.isNullOrEmpty()) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceManager.kt index 60ddf3987d7..7981197416b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceManager.kt @@ -23,9 +23,7 @@ import com.intellij.openapi.util.Key import com.intellij.openapi.util.TextRange import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.awt.RelativePoint -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.Reference -import software.amazon.awssdk.services.codewhispererruntime.model.Span +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.getPopupPositionAboveText import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.getRelativePathToContentRoot import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints @@ -68,22 +66,18 @@ class CodeWhispererCodeReferenceManager(private val project: Project) { toolWindow?.show() } - fun insertCodeReference(originalCode: String, references: List, editor: Editor, caretPosition: CaretPosition, detail: Completion?) { + fun insertCodeReference(originalCode: String, references: List?, editor: Editor, caretPosition: CaretPosition) { val startOffset = caretPosition.offset val relativePath = getRelativePathToContentRoot(editor) - references.forEachIndexed { i, reference -> - val start = startOffset + reference.recommendationContentSpan().start() - val end = startOffset + reference.recommendationContentSpan().end() + references?.forEachIndexed { i, reference -> + // TODO YUX: validate this + val start = startOffset + reference.position.startCharacter + val end = startOffset + reference.position.endCharacter val lineNums = getReferenceLineNums(editor, start, end) - // There is an unformatted recommendation(directly from response) and reformatted one. We want to get - // the line number, start/end offset of the reformatted one because it's the one inserted to the editor. - // However, the one that shows in the tool window record should show the original recommendation, as below. - val originalContentLines = if (detail != null) { - getOriginalContentLines(detail, i) - } else { - getOriginalContentLines(originalCode, reference.recommendationContentSpan()) - } + val originalContentLines = originalCode + .substring(reference.position.startCharacter, reference.position.endCharacter) + .split("\n") addReferenceLogPanelEntry(reference, relativePath, lineNums, originalContentLines) @@ -91,7 +85,7 @@ class CodeWhispererCodeReferenceManager(private val project: Project) { } } - fun addReferenceLogPanelEntry(reference: Reference, relativePath: String?, lineNums: String?, originalContentLines: List?) { + fun addReferenceLogPanelEntry(reference: InlineCompletionReference, relativePath: String?, lineNums: String?, originalContentLines: List?) { codeReferenceComponents.contentPanel.apply { add( codeReferenceComponents.codeReferenceRecordPanel(reference, relativePath, lineNums), @@ -114,18 +108,17 @@ class CodeWhispererCodeReferenceManager(private val project: Project) { fun insertCodeReference(states: InvocationContext, selectedIndex: Int) { val (requestContext, _, recommendationContext) = states val (_, editor, _, caretPosition) = requestContext - val (_, detail, reformattedDetail) = recommendationContext.details[selectedIndex] - insertCodeReference(detail.content(), reformattedDetail.references(), editor, caretPosition, detail) + val (_, completion) = recommendationContext.details[selectedIndex] + insertCodeReference(completion.insertText, completion.references, editor, caretPosition) } fun insertCodeReference(states: InvocationContextNew, previews: List, selectedIndex: Int) { val detail = previews[selectedIndex].detail insertCodeReference( - detail.recommendation.content(), - detail.reformatted.references(), + detail.completion.insertText, + detail.completion.references, states.requestContext.editor, states.requestContext.caretPosition, - detail.recommendation ) } @@ -140,22 +133,12 @@ class CodeWhispererCodeReferenceManager(private val project: Project) { return lineNums } - fun getOriginalContentLines(detail: Completion, i: Int): List { - val originalSpan = detail.references()[i].recommendationContentSpan() - return getOriginalContentLines(detail.content(), originalSpan) - } - - private fun getOriginalContentLines(originalCode: String, originalSpan: Span): List = - originalCode - .substring(originalSpan.start(), originalSpan.end()) - .split("\n") - - private fun insertHighLightContext(editor: Editor, start: Int, end: Int, reference: Reference) { + private fun insertHighLightContext(editor: Editor, start: Int, end: Int, reference: InlineCompletionReference) { val codeContent = editor.document.getText(TextRange.create(start, end)) val referenceContent = message( "codewhisperer.toolwindow.popup.text", - reference.licenseName(), - reference.repository() + reference.licenseName, + reference.referenceName ) val highlighter = editor.markupModel.addRangeHighlighter( start, 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 3440c326945..c8da6b85a69 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 @@ -8,8 +8,6 @@ import com.intellij.openapi.editor.markup.EffectType import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.ui.JBColor import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask @@ -21,18 +19,13 @@ object CodeWhispererConstants { const val CHARACTERS_LIMIT = 10240 const val BEGINNING_OF_FILE = 0 const val FILENAME_CHARS_LIMIT = 1024 - const val INVOCATION_KEY_INTERVAL_THRESHOLD = 15 val SPECIAL_CHARACTERS_LIST = listOf("{", "[", "(", ":") val PAIRED_BRACKETS = mapOf('{' to '}', '(' to ')', '[' to ']', '<' to '>') val PAIRED_QUOTES = setOf('"', '\'', '`') - const val INVOCATION_TIME_INTERVAL_THRESHOLD = 2 const val LEFT_CONTEXT_ON_CURRENT_LINE = 50 const val POPUP_INFO_TEXT_SIZE = 11f const val POPUP_BUTTON_TEXT_SIZE = 12f - const val POPUP_DELAY: Long = 250 const val POPUP_DELAY_CHECK_INTERVAL: Long = 25 - const val IDLE_TIME_CHECK_INTERVAL: Long = 25 - const val SUPPLEMETAL_CONTEXT_BUFFER = 10L val AWSTemplateKeyWordsRegex = Regex("(AWSTemplateFormatVersion|Resources|AWS::|Description)") val AWSTemplateCaseInsensitiveKeyWordsRegex = Regex("(cloudformation|cfn|template|description)") @@ -51,15 +44,10 @@ object CodeWhispererConstants { "vcpkg.json" ) - // TODO: this is currently set to 2050 to account for the server side 0.5 TPS and and extra 50 ms buffer to - // avoid ThrottlingException as much as possible. - const val INVOCATION_INTERVAL: Long = 2050 - val runScanKey = DataKey.create("amazonq.codescan.run") val scanResultsKey = DataKey.create("amazonq.codescan.result") val scanScopeKey = DataKey.create("amazonq.codescan.scope") - const val Q_CUSTOM_LEARN_MORE_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/customizations.html" const val Q_SUPPORTED_LANG_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html" const val CODEWHISPERER_CODE_SCAN_LEARN_MORE_URI = "https://docs.aws.amazon.com/codewhisperer/latest/userguide/security-scans.html" const val CODEWHISPERER_ONBOARDING_DOCUMENTATION_URI = "https://docs.aws.amazon.com/codewhisperer/latest/userguide/features.html" @@ -157,39 +145,9 @@ object CodeWhispererConstants { val Sigv4ClientRegion = Region.US_EAST_1 } - object Customization { - private const val noAccessToCustomizationMessage = "Your account is not authorized to use CodeWhisperer Enterprise." - private const val invalidCustomizationMessage = "You are not authorized to access" - - val noAccessToCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> - if (e !is CodeWhispererRuntimeException) { - false - } else { - e is AccessDeniedException && (e.message?.contains(noAccessToCustomizationMessage, ignoreCase = true) ?: false) - } - } - - val invalidCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> - if (e !is CodeWhispererRuntimeException) { - false - } else { - e is AccessDeniedException && (e.message?.contains(invalidCustomizationMessage, ignoreCase = true) ?: false) - } - } - } - object CrossFile { - const val CHUNK_SIZE = 60 const val NUMBER_OF_LINE_IN_CHUNK = 50 const val NUMBER_OF_CHUNK_TO_FETCH = 3 - const val MAX_TOTAL_LENGTH = 20480 - const val MAX_LENGTH_PER_CHUNK = 10240 - const val MAX_CONTEXT_COUNT = 5 - } - - object Utg { - const val UTG_SEGMENT_SIZE = 10200 - const val UTG_PREFIX = "UTG\n" } object TryExampleFileContent { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt index e825535d5db..32da3d6075a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt @@ -3,505 +3,24 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.util -import com.intellij.ide.actions.CopyContentRootPathProvider -import com.intellij.openapi.application.readAction -import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile -import com.intellij.util.gist.GistManager -import com.intellij.util.io.DataExternalizer -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.yield -import org.jetbrains.annotations.VisibleForTesting -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.services.amazonq.project.ProjectContextController import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.SUPPLEMETAL_CONTEXT_BUFFER -import java.io.DataInput -import java.io.DataOutput -import java.util.Collections -import kotlin.coroutines.coroutineContext -import kotlin.time.measureTimedValue - -private val contentRootPathProvider = CopyContentRootPathProvider() - -private val codewhispererCodeChunksIndex = GistManager.getInstance() - .newPsiFileGist("psi to code chunk index", 0, CodeWhispererCodeChunkExternalizer) { psiFile -> - runBlocking { - val fileCrawler = psiFile.programmingLanguage().fileCrawler - val fileProducers = listOf List> { psiFile -> fileCrawler.listCrossFileCandidate(psiFile) } - FileContextProvider.getInstance(psiFile.project).extractCodeChunksFromFiles(psiFile, fileProducers) - } - } - -private object CodeWhispererCodeChunkExternalizer : DataExternalizer> { - override fun save(out: DataOutput, value: List) { - out.writeInt(value.size) - value.forEach { chunk -> - out.writeUTF(chunk.path) - out.writeUTF(chunk.content) - out.writeUTF(chunk.nextChunk) - } - } - - override fun read(`in`: DataInput): List { - val result = mutableListOf() - val size = `in`.readInt() - repeat(size) { - result.add( - Chunk( - path = `in`.readUTF(), - content = `in`.readUTF(), - nextChunk = `in`.readUTF() - ) - ) - } - - return result - } -} /** * [extractFileContext] will extract the context from a psi file provided - * [extractSupplementalFileContext] supplemental means file context extracted from files other than the provided one */ interface FileContextProvider { fun extractFileContext(editor: Editor, psiFile: PsiFile): FileContextInfo - suspend fun extractSupplementalFileContext(psiFile: PsiFile, fileContext: FileContextInfo, timeout: Long): SupplementalContextInfo? - - suspend fun extractCodeChunksFromFiles(psiFile: PsiFile, fileProducers: List List>): List - - /** - * It will actually delegate to invoke corresponding [CodeWhispererFileCrawler.isTestFile] for each language - * as different languages have their own naming conventions. - */ - fun isTestFile(psiFile: PsiFile): Boolean - companion object { fun getInstance(project: Project): FileContextProvider = project.service() } } -class DefaultCodeWhispererFileContextProvider(private val project: Project) : FileContextProvider { +class DefaultCodeWhispererFileContextProvider : FileContextProvider { override fun extractFileContext(editor: Editor, psiFile: PsiFile): FileContextInfo = CodeWhispererEditorUtil.getFileContextInfo(editor, psiFile) - - /** - * codewhisperer extract the supplemental context with 2 different approaches depending on what type of file the target file is. - * 1. source file -> explore files/classes imported from the target file + files within the same project root - * 2. test file -> explore "focal file" if applicable, otherwise fall back to most "relevant" file. - * for focal files, e.g. "MainTest.java" -> "Main.java", "test_main.py" -> "main.py" - * for the most relevant file -> we extract "keywords" from files opened in editor then get the one with the highest similarity with target file - */ - override suspend fun extractSupplementalFileContext(psiFile: PsiFile, targetContext: FileContextInfo, timeout: Long): SupplementalContextInfo? { - val startFetchingTimestamp = System.currentTimeMillis() - val isTst = readAction { isTestFile(psiFile) } - return try { - val language = targetContext.programmingLanguage - - val supplementalContext = if (isTst) { - when (shouldFetchUtgContext(language)) { - true -> withTimeout(timeout) { extractSupplementalFileContextForTst(psiFile, targetContext) } - false -> SupplementalContextInfo.emptyUtgFileContextInfo(targetContext.filename) - null -> { - LOG.debug { "UTG is not supporting ${targetContext.programmingLanguage.languageId}" } - null - } - } - } else { - when (shouldFetchCrossfileContext(language)) { - // we need this buffer 10ms as when project context timeout by 50ms, - // the entire [extractSupplementalFileContextForSrc] call will time out and not even return openTabsContext - true -> withTimeout(timeout + SUPPLEMETAL_CONTEXT_BUFFER) { extractSupplementalFileContextForSrc(psiFile, targetContext) } - false -> SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - null -> { - LOG.debug { "Crossfile is not supporting ${targetContext.programmingLanguage.languageId}" } - null - } - } - } - - return supplementalContext?.let { - val latency = System.currentTimeMillis() - startFetchingTimestamp - if (it.contents.isNotEmpty()) { - val logStr = buildString { - append( - """Q inline completion supplemental context: - | Strategy: ${it.strategy}, - | Latency: $latency ms, - | Contents: ${it.contents.size} chunks, - | ContentLength: ${it.contentLength} chars, - | TargetFile: ${it.targetFileName}, - """.trimMargin() - ) - it.contents.forEachIndexed { index, chunk -> - append( - """ - | - | Chunk $index: - | path = ${chunk.path}, - | score = ${chunk.score}, - | contentLength = ${chunk.content.length} - | - """.trimMargin() - ) - } - } - - LOG.info { logStr } - } else { - LOG.warn { "Failed to fetch supplemental context, empty list." } - } - - it.copy(latency = latency) - } - } catch (e: TimeoutCancellationException) { - LOG.debug { - "Supplemental context fetch timed out in ${System.currentTimeMillis() - startFetchingTimestamp}ms" - } - SupplementalContextInfo( - isUtg = isTst, - contents = emptyList(), - latency = System.currentTimeMillis() - startFetchingTimestamp, - targetFileName = targetContext.filename, - strategy = if (isTst) UtgStrategy.Empty else CrossFileStrategy.Empty - ) - } catch (e: Exception) { - throw e - } - } - - override suspend fun extractCodeChunksFromFiles(psiFile: PsiFile, fileProducers: List List>): List { - val hasUsed = Collections.synchronizedSet(mutableSetOf()) - val chunks = mutableListOf() - - for (fileProducer in fileProducers) { - yield() - val files = fileProducer(psiFile) - files.forEach { file -> - yield() - if (hasUsed.contains(file)) { - return@forEach - } - val relativePath = runReadAction { contentRootPathProvider.getPathToElement(project, file, null) ?: file.path } - chunks.addAll(file.toCodeChunk(relativePath)) - hasUsed.add(file) - if (chunks.size > CodeWhispererConstants.CrossFile.CHUNK_SIZE) { - return chunks.take(CodeWhispererConstants.CrossFile.CHUNK_SIZE) - } - } - } - - return chunks.take(CodeWhispererConstants.CrossFile.CHUNK_SIZE) - } - - override fun isTestFile(psiFile: PsiFile) = psiFile.programmingLanguage().fileCrawler.isTestFile(psiFile.virtualFile, psiFile.project) - - suspend fun extractSupplementalFileContextForSrc(psiFile: PsiFile, targetContext: FileContextInfo): SupplementalContextInfo { - if (!targetContext.programmingLanguage.isSupplementalContextSupported()) { - return SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - } - - val query = generateQuery(targetContext) - - val contexts = withContext(coroutineContext) { - val projectContextDeferred1 = async { - val timedCodemapContext = measureTimedValue { fetchProjectContext(query, psiFile, targetContext) } - val codemapContext = timedCodemapContext.value - LOG.debug { - buildString { - append("time elapse for fetching project context=${timedCodemapContext.duration.inWholeMilliseconds}ms; ") - append("numberOfChunks=${codemapContext.contents.size}; ") - append("totalLength=${codemapContext.contentLength}") - } - } - - codemapContext - } - - val openTabsContextDeferred1 = async { - val timedOpentabContext = measureTimedValue { fetchOpenTabsContext(query, psiFile, targetContext) } - val opentabContext = timedOpentabContext.value - LOG.debug { - buildString { - append("time elapse for open tabs context=${timedOpentabContext.duration.inWholeMilliseconds}ms; ") - append("numberOfChunks=${opentabContext.contents.size}; ") - append("totalLength=${opentabContext.contentLength}") - } - } - - opentabContext - } - - awaitAll(projectContextDeferred1, openTabsContextDeferred1) - } - - val projectContext = contexts.find { it.strategy == CrossFileStrategy.Codemap } - val openTabsContext = contexts.find { it.strategy == CrossFileStrategy.OpenTabsBM25 } - - /** - * We're using both codemap and opentabs context - * 1. If both are present, codemap should live in the first of supplemental context list, i.e [codemap, opentabs_0, opentabs_1...] with strategy name codemap - * 2. If only one is present, return the one present with corresponding strategy name, either codemap or opentabs - * 3. If none is present, return empty list with strategy name empty - * - * Service will throw 400 error when context length is greater than 20480, drop the last chunk until the total length fits in the cap - */ - val contextBeforeTruncation = when { - projectContext == null && openTabsContext == null -> SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - - projectContext != null && openTabsContext != null -> { - val context1 = projectContext.contents - val context2 = openTabsContext.contents - val mergedContext = (context1 + context2).filter { it.content.isNotEmpty() } - - val strategy = if (projectContext.contentLength != 0 && openTabsContext.contentLength != 0) { - CrossFileStrategy.Codemap - } else if (projectContext.contentLength != 0) { - CrossFileStrategy.Codemap - } else if (openTabsContext.contentLength != 0) { - CrossFileStrategy.OpenTabsBM25 - } else { - CrossFileStrategy.Empty - } - - SupplementalContextInfo( - isUtg = false, - contents = mergedContext, - targetFileName = targetContext.filename, - strategy = strategy - ) - } - - projectContext != null -> { - return if (projectContext.contentLength == 0) { - SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - } else { - SupplementalContextInfo( - isUtg = false, - contents = projectContext.contents, - targetFileName = targetContext.filename, - strategy = CrossFileStrategy.Codemap - ) - } - } - - openTabsContext != null -> { - return if (openTabsContext.contentLength == 0) { - SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - } else { - SupplementalContextInfo( - isUtg = false, - contents = openTabsContext.contents, - targetFileName = targetContext.filename, - strategy = CrossFileStrategy.OpenTabsBM25 - ) - } - } - - else -> SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - } - - return truncateContext(contextBeforeTruncation) - } - - /** - * Requirement - * - Maximum 5 supplemental context. - * - Each chunk can't exceed 10240 characters - * - Sum of all chunks can't exceed 20480 characters - */ - fun truncateContext(context: SupplementalContextInfo): SupplementalContextInfo { - var c = context.contents.map { - return@map if (it.content.length > CodeWhispererConstants.CrossFile.MAX_LENGTH_PER_CHUNK) { - it.copy(content = truncateLineByLine(it.content, CodeWhispererConstants.CrossFile.MAX_LENGTH_PER_CHUNK)) - } else { - it - } - } - - if (c.size > CodeWhispererConstants.CrossFile.MAX_CONTEXT_COUNT) { - c = c.subList(0, CodeWhispererConstants.CrossFile.MAX_CONTEXT_COUNT) - } - - var curTotalLength = c.sumOf { it.content.length } - while (curTotalLength >= CodeWhispererConstants.CrossFile.MAX_TOTAL_LENGTH) { - val last = c.last() - c = c.dropLast(1) - curTotalLength -= last.content.length - } - - return context.copy(contents = c) - } - - @VisibleForTesting - suspend fun fetchProjectContext(query: String, psiFile: PsiFile, targetContext: FileContextInfo): SupplementalContextInfo { - val response = ProjectContextController.getInstance(project).queryInline(query, psiFile.virtualFile?.path.orEmpty()) - - return SupplementalContextInfo( - isUtg = false, - contents = response.map { - Chunk( - content = it.content, - path = it.filePath, - nextChunk = it.content, - score = it.score - ) - }, - targetFileName = targetContext.filename, - strategy = CrossFileStrategy.Codemap - ) - } - - @VisibleForTesting - suspend fun fetchOpenTabsContext(query: String, psiFile: PsiFile, targetContext: FileContextInfo): SupplementalContextInfo { - // step 1: prepare data - val first60Chunks: List = try { - runReadAction { codewhispererCodeChunksIndex.getFileData(psiFile) } - } catch (e: TimeoutCancellationException) { - throw e - } - - yield() - - if (first60Chunks.isEmpty()) { - LOG.warn { - "0 chunks was found for supplemental context, fileName=${targetContext.filename}, " + - "programmingLanaugage: ${targetContext.programmingLanguage}" - } - return SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename) - } - - // we need to keep the reference to Chunk object because we will need to get "nextChunk" later after calculation - val contentToChunk = first60Chunks.associateBy { it.content } - - // BM250 only take list of string as argument - // step 2: bm25 calculation - val top3Chunks: List = BM250kapi(first60Chunks.map { it.content }).topN(query) - - yield() - - // we use nextChunk as supplemental context - val crossfileContext = top3Chunks.mapNotNull { bm25Result -> - contentToChunk[bm25Result.docString]?.let { - if (it.nextChunk.isNotBlank()) { - Chunk(content = it.nextChunk, path = it.path, score = bm25Result.score) - } else { - null - } - } - } - - return SupplementalContextInfo( - isUtg = false, - contents = crossfileContext, - targetFileName = targetContext.filename, - strategy = CrossFileStrategy.OpenTabsBM25 - ) - } - - @VisibleForTesting - fun extractSupplementalFileContextForTst(psiFile: PsiFile, targetContext: FileContextInfo): SupplementalContextInfo { - if (!targetContext.programmingLanguage.isUTGSupported()) { - return SupplementalContextInfo.emptyUtgFileContextInfo(targetContext.filename) - } - - val utgCandidateResult = targetContext.programmingLanguage.fileCrawler.listUtgCandidate(psiFile) - val focalFile = utgCandidateResult.vfile - val strategy = utgCandidateResult.strategy - - return focalFile?.let { file -> - runReadAction { - val relativePath = contentRootPathProvider.getPathToElement(project, file, null) ?: file.path - val content = file.content() - - val utgContext = if (content.isBlank()) { - emptyList() - } else { - listOf( - Chunk( - content = CodeWhispererConstants.Utg.UTG_PREFIX + file.content().let { - it.substring( - 0, - minOf(it.length, CodeWhispererConstants.Utg.UTG_SEGMENT_SIZE) - ) - }, - path = relativePath - ) - ) - } - - SupplementalContextInfo( - isUtg = true, - contents = utgContext, - targetFileName = targetContext.filename, - strategy = strategy - ) - } - } ?: run { - return SupplementalContextInfo.emptyUtgFileContextInfo(targetContext.filename) - } - } - - // takeLast NUMBER_OF_LINE_IN_CHUNK of lines (exclusing current line) in left context as the query - fun generateQuery(fileContext: FileContextInfo) = fileContext.caretContext.leftFileContext - .split("\n") - .dropLast(1) - .takeLast(CodeWhispererConstants.CrossFile.NUMBER_OF_LINE_IN_CHUNK) - .joinToString("\n") - - companion object { - private val LOG = getLogger() - - fun shouldFetchUtgContext(language: CodeWhispererProgrammingLanguage): Boolean? { - if (!language.isUTGSupported()) { - return null - } - - return when (language) { - is CodeWhispererJava -> true - else -> false - } - } - - fun shouldFetchCrossfileContext(language: CodeWhispererProgrammingLanguage): Boolean? { - if (!language.isSupplementalContextSupported()) { - return null - } - - return when (language) { - is CodeWhispererJava, - is CodeWhispererPython, - is CodeWhispererJavaScript, - is CodeWhispererTypeScript, - is CodeWhispererJsx, - is CodeWhispererTsx, - -> true - - else -> false - } - } - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileCrawler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileCrawler.kt deleted file mode 100644 index b5a67642e44..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileCrawler.kt +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessProjectDir -import com.intellij.openapi.roots.TestSourcesFilter -import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiManager -import software.aws.toolkits.core.utils.tryOrNull -import software.aws.toolkits.jetbrains.services.codewhisperer.model.ListUtgCandidateResult - -/** - * An interface define how do we parse and fetch files provided a psi file or project - * since different language has its own way importing other files or its own naming style for test file - */ -interface FileCrawler { - fun listFilesUnderProjectRoot(project: Project): List - - /** - * should be invoked at test files e.g. MainTest.java, or test_main.py - * @param target psi of the test file we are searching with, e.g. MainTest.java - * @return its source file e.g. Main.java, main.py or most relevant file if any - */ - fun listUtgCandidate(target: PsiFile): ListUtgCandidateResult - - /** - * List files opened in the editors and sorted by file distance @see [CodeWhispererFileCrawler.getFileDistance] - * @return opened files and satisfy the following conditions - * (1) not the input file - * (2) with the same file extension as the input file has - * (3) non-test file which will be determined by [FileCrawler.isTestFile] - * (4) writable file - */ - fun listCrossFileCandidate(target: PsiFile): List - - /** - * Determine if the file given is test file or not based on its path and file name - */ - fun isTestFile(target: VirtualFile, project: Project): Boolean -} - -class NoOpFileCrawler : FileCrawler { - override fun listFilesUnderProjectRoot(project: Project): List = emptyList() - - override fun listUtgCandidate(target: PsiFile) = ListUtgCandidateResult(null, UtgStrategy.Empty) - - override fun listCrossFileCandidate(target: PsiFile): List = emptyList() - - override fun isTestFile(target: VirtualFile, project: Project): Boolean = false -} - -abstract class CodeWhispererFileCrawler : FileCrawler { - abstract val fileExtension: String - abstract val dialects: Set - abstract val testFileNamingPatterns: List - - override fun isTestFile(target: VirtualFile, project: Project): Boolean { - val filePath = target.path - - // if file path itself explicitly explains the file is under test sources - if (TestSourcesFilter.isTestSources(target, project) || - filePath.contains("""test/""", ignoreCase = true) || - filePath.contains("""tst/""", ignoreCase = true) || - filePath.contains("""tests/""", ignoreCase = true) - ) { - return true - } - - // no explicit clue from the file path, use regexes based on naming conventions - return testFileNamingPatterns.any { it.matches(target.name) } - } - - override fun listFilesUnderProjectRoot(project: Project): List = project.guessProjectDir()?.let { rootDir -> - VfsUtil.collectChildrenRecursively(rootDir).filter { - // TODO: need to handle cases js vs. jsx, ts vs. tsx when we enable js/ts utg since we likely have different file extensions - it.path.endsWith(fileExtension) - } - }.orEmpty() - - override fun listCrossFileCandidate(target: PsiFile): List { - /** - * [PsiFile.getVirtualFile] - * PsiFile.virtualFile will return the virtual file, or null if the file exists only in memory - * If you want to get a non-null virtual file consider using FileViewProvider. getVirtualFile() - */ - val targetVirtualFile = target.virtualFile ?: target.viewProvider.virtualFile - - val openedFiles = runReadAction { - FileEditorManager.getInstance(target.project).openFiles.toList().filter { - it.name != targetVirtualFile.name && - isSameDialect(it.extension) && - !isTestFile(it, target.project) - } - } - - val fileToFileDistanceList = runReadAction { - openedFiles.map { - return@map it to CodeWhispererFileCrawler.getFileDistance(fileA = targetVirtualFile, fileB = it) - } - } - - return fileToFileDistanceList.sortedBy { it.second }.map { it.first } - } - - override fun listUtgCandidate(target: PsiFile): ListUtgCandidateResult { - val byName = findSourceFileByName(target) - if (byName != null) { - return ListUtgCandidateResult(byName, UtgStrategy.ByName) - } - - val byContent = findSourceFileByContent(target) - if (byContent != null) { - return ListUtgCandidateResult(byContent, UtgStrategy.ByContent) - } - - return ListUtgCandidateResult(null, UtgStrategy.Empty) - } - - abstract fun findSourceFileByName(target: PsiFile): VirtualFile? - - abstract fun findSourceFileByContent(target: PsiFile): VirtualFile? - - // TODO: may need to update when we enable JS/TS UTG, since we have to factor in .jsx/.tsx combinations - fun guessSourceFileName(tstFileName: String): String? { - val srcFileName = tryOrNull { - testFileNamingPatterns.firstNotNullOf { regex -> - regex.find(tstFileName)?.groupValues?.let { groupValues -> - groupValues.get(1) + groupValues.get(2) - } - } - } - - return srcFileName - } - - private fun isSameDialect(fileExt: String?): Boolean = fileExt?.let { - dialects.contains(fileExt) - } ?: false - - companion object { - // TODO: move to CodeWhispererUtils.kt - /** - * @param target will be the source of keywords - * @param keywordProducer defines how we generate keywords from the target - * @return return the file with the highest substring matching from all opened files with the same file extension - */ - fun searchRelevantFileInEditors(target: PsiFile, keywordProducer: (psiFile: PsiFile) -> List): VirtualFile? { - val project = target.project - val targetElements = keywordProducer(target) - - return runReadAction { - FileEditorManager.getInstance(project).openFiles - .filter { openedFile -> - openedFile.name != target.virtualFile.name && openedFile.extension == target.virtualFile.extension - } - .mapNotNull { openedFile -> PsiManager.getInstance(project).findFile(openedFile) } - .maxByOrNull { - val elementsToCheck = keywordProducer(it) - countSubstringMatches(targetElements, elementsToCheck) - }?.virtualFile - } - } - - // TODO: move to CodeWhispererUtils.kt - /** - * how many elements in elementsToCheck is contained (as substring) in targetElements - */ - fun countSubstringMatches(targetElements: List, elementsToCheck: List): Int = elementsToCheck.fold(0) { acc, elementToCheck -> - val hasTarget = targetElements.any { it.contains(elementToCheck, ignoreCase = true) } - if (hasTarget) { - acc + 1 - } else { - acc - } - } - - /** - * For [LocalFileSystem](implementation of virtual file system), the path will be an absolute file path with file separator characters replaced - * by forward slash "/" - * @see [VirtualFile.getPath] - */ - fun getFileDistance(fileA: VirtualFile, fileB: VirtualFile): Int { - val targetFilePaths = fileA.path.split("/").dropLast(1) - val candidateFilePaths = fileB.path.split("/").dropLast(1) - - var i = 0 - while (i < minOf(targetFilePaths.size, candidateFilePaths.size)) { - val dir1 = targetFilePaths[i] - val dir2 = candidateFilePaths[i] - - if (dir1 != dir2) { - break - } - - i++ - } - - return targetFilePaths.subList(fromIndex = i, toIndex = targetFilePaths.size).size + - candidateFilePaths.subList(fromIndex = i, toIndex = candidateFilePaths.size).size - } - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt index 566d9518ccb..20682bfbe96 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt @@ -3,16 +3,12 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.util -import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.lookup.LookupManager import com.intellij.ide.BrowserUtil -import com.intellij.lang.annotation.HighlightSeverity import com.intellij.notification.NotificationAction import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil @@ -25,11 +21,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.yield -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.IdeDiagnostic import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference -import software.amazon.awssdk.services.codewhispererruntime.model.Position -import software.amazon.awssdk.services.codewhispererruntime.model.Range import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection @@ -46,11 +38,11 @@ import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnecti import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnectionType import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionItem import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.learn.LearnCodeWhispererManager.Companion.taskTypeToFilename import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker.Companion.levenshteinChecker import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.isTelemetryEnabled import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CrossFile.NUMBER_OF_CHUNK_TO_FETCH import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CrossFile.NUMBER_OF_LINE_IN_CHUNK @@ -187,8 +179,8 @@ fun VirtualFile.toCodeChunk(path: String): Sequence = sequence { fun VirtualFile.isWithin(ancestor: VirtualFile): Boolean = VfsUtilCore.isAncestor(ancestor, this, false) object CodeWhispererUtil { - fun getCompletionType(completion: Completion): CodewhispererCompletionType { - val content = completion.content() + fun getCompletionType(completion: InlineCompletionItem): CodewhispererCompletionType { + val content = completion.insertText val nonBlankLines = content.split("\n").count { it.isNotBlank() } return when { @@ -342,19 +334,6 @@ object CodeWhispererUtil { } } - // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace), - // and thus the unmodified part of recommendation length can be deducted/approximated - // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3 - // ex. (modified == original): originalRecom: helloworld -> modifiedRecom: HelloWorld, distance = 2, delta = 10 - 2 = 8 - // ex. (modified < original): originalRecom: CodeWhisperer -> modifiedRecom: CODE, distance = 12, delta = 13 - 12 = 1 - fun getUnmodifiedAcceptedCharsCount(originalRecommendation: String, modifiedRecommendation: String): Int { - val editDistance = getEditDistance(modifiedRecommendation, originalRecommendation).toInt() - return maxOf(originalRecommendation.length, modifiedRecommendation.length) - editDistance - } - - private fun getEditDistance(modifiedString: String, originalString: String): Double = - levenshteinChecker.distance(modifiedString, originalString) - fun setIntelliSensePopupAlpha(editor: Editor, alpha: Float) { ComponentUtil.getWindow(LookupManager.getActiveLookup(editor)?.component)?.let { WindowManager.getInstance().setAlphaModeRatio(it, alpha) @@ -368,88 +347,3 @@ object CodeWhispererUtil { enum class CaretMovement { NO_CHANGE, MOVE_FORWARD, MOVE_BACKWARD } - -fun getDiagnosticsType(message: String): String { - val lowercaseMessage = message.lowercase() - - val diagnosticPatterns = mapOf( - "TYPE_ERROR" to listOf("type", "cast"), - "SYNTAX_ERROR" to listOf("expected", "indent", "syntax"), - "REFERENCE_ERROR" to listOf("undefined", "not defined", "undeclared", "reference", "symbol"), - "BEST_PRACTICE" to listOf("deprecated", "unused", "uninitialized", "not initialized"), - "SECURITY" to listOf("security", "vulnerability") - ) - - return diagnosticPatterns - .entries - .firstOrNull { (_, keywords) -> - keywords.any { lowercaseMessage.contains(it) } - } - ?.key ?: "OTHER" -} - -fun convertSeverity(severity: HighlightSeverity): String = when { - severity == HighlightSeverity.ERROR -> "ERROR" - severity == HighlightSeverity.WARNING || - severity == HighlightSeverity.WEAK_WARNING -> "WARNING" - severity == HighlightSeverity.INFORMATION -> "INFORMATION" - severity.toString().contains("TEXT", ignoreCase = true) -> "HINT" - severity == HighlightSeverity.INFO -> "INFORMATION" - // For severities that might indicate performance issues - severity.toString().contains("PERFORMANCE", ignoreCase = true) -> "WARNING" - // For deprecation warnings - severity.toString().contains("DEPRECATED", ignoreCase = true) -> "WARNING" - // Default case - else -> "INFORMATION" -} - -fun getDocumentDiagnostics(document: Document, project: Project): List = runCatching { - DocumentMarkupModel.forDocument(document, project, true) - .allHighlighters - .mapNotNull { it.errorStripeTooltip as? HighlightInfo } - .filter { !it.description.isNullOrEmpty() } - .map { info -> - val startLine = document.getLineNumber(info.startOffset) - val endLine = document.getLineNumber(info.endOffset) - - IdeDiagnostic.builder() - .ideDiagnosticType(getDiagnosticsType(info.description)) - .severity(convertSeverity(info.severity)) - .source(info.inspectionToolId) - .range( - Range.builder() - .start( - Position.builder() - .line(startLine) - .character(document.getLineStartOffset(startLine)) - .build() - ) - .end( - Position.builder() - .line(endLine) - .character(document.getLineStartOffset(endLine)) - .build() - ) - .build() - ) - .build() - } -}.getOrElse { e -> - getLogger().warn { "Failed to get document diagnostics ${e.message}" } - emptyList() -} - -data class DiagnosticDifferences( - val added: List, - val removed: List, -) - -fun serializeDiagnostics(diagnostic: IdeDiagnostic): String = "${diagnostic.source()}-${diagnostic.severity()}-${diagnostic.ideDiagnosticType()}" - -fun getDiagnosticDifferences(oldDiagnostic: List, newDiagnostic: List): DiagnosticDifferences { - val oldSet = oldDiagnostic.map { i -> serializeDiagnostics(i) }.toSet() - val newSet = newDiagnostic.map { i -> serializeDiagnostics(i) }.toSet() - val added = newDiagnostic.filter { i -> !oldSet.contains(serializeDiagnostics(i)) }.distinctBy { serializeDiagnostics(it) } - val removed = oldDiagnostic.filter { i -> !newSet.contains(serializeDiagnostics(i)) }.distinctBy { serializeDiagnostics(it) } - return DiagnosticDifferences(added, removed) -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavaCodeWhispererFileCrawler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavaCodeWhispererFileCrawler.kt deleted file mode 100644 index 93d6b9c7fff..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavaCodeWhispererFileCrawler.kt +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.openapi.project.rootManager -import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes -import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.warn -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.ClassResolverKey -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispereJavaClassResolver -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispererClassResolver - -object JavaCodeWhispererFileCrawler : CodeWhispererFileCrawler() { - override val fileExtension: String = "java" - override val dialects: Set = setOf("java") - override val testFileNamingPatterns = listOf( - Regex("""^(.+)Test(\.java)$"""), - Regex("""^(.+)Tests(\.java)$""") - ) - - // psiFile = "MainTest.java", targetFileName = "Main.java" - override fun findSourceFileByName(target: PsiFile): VirtualFile? = - guessSourceFileName(target.virtualFile.name)?.let { srcName -> - val module = ModuleUtilCore.findModuleForFile(target) - - module?.rootManager?.getSourceRoots(JavaModuleSourceRootTypes.PRODUCTION)?.let { srcRoot -> - srcRoot - .map { root -> VfsUtil.collectChildrenRecursively(root) } - .flatten() - .find { !it.isDirectory && it.isWritable && it.name == srcName } - } - } - - /** - * check files in editors and pick one which has most substring matches to the target - */ - override fun findSourceFileByContent(target: PsiFile): VirtualFile? = searchRelevantFileInEditors(target) { myPsiFile -> - CodeWhispererClassResolver.EP_NAME.findFirstSafe { it is CodeWhispereJavaClassResolver }?.let { - val classAndMethods = it.resolveClassAndMembers(myPsiFile) - val clazz = classAndMethods[ClassResolverKey.ClassName].orEmpty() - val methods = classAndMethods[ClassResolverKey.MethodName].orEmpty() - - clazz + methods - } ?: run { - getLogger().warn { - "could not resolve correct CwsprClassResolver, available CwsprClassResolver=${CodeWhispererClassResolver.EP_NAME.extensionList}" - } - emptyList() - } - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavascriptCodeWhispererFileCrawler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavascriptCodeWhispererFileCrawler.kt deleted file mode 100644 index cf4ddbe6503..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/JavascriptCodeWhispererFileCrawler.kt +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile - -object JavascriptCodeWhispererFileCrawler : CodeWhispererFileCrawler() { - override val fileExtension: String = "js" - override val dialects: Set = setOf("js", "jsx") - override val testFileNamingPatterns: List = listOf( - Regex("""^(.+)\.(?i:t)est(\.js|\.jsx)$"""), - Regex("""^(.+)\.(?i:s)pec(\.js|\.jsx)$""") - ) - - override fun findSourceFileByName(target: PsiFile): VirtualFile? = null - - override fun findSourceFileByContent(target: PsiFile): VirtualFile? = null -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/PythonCodeWhispererFileCrawler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/PythonCodeWhispererFileCrawler.kt deleted file mode 100644 index 56130b2dd40..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/PythonCodeWhispererFileCrawler.kt +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.ClassResolverKey -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispererClassResolver -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispererPythonClassResolver - -object PythonCodeWhispererFileCrawler : CodeWhispererFileCrawler() { - override val fileExtension: String = "py" - override val dialects: Set = setOf("py") - override val testFileNamingPatterns: List = listOf( - Regex("""^test_(.+)(\.py)$"""), - Regex("""^(.+)_test(\.py)$""") - ) - - override fun findSourceFileByName(target: PsiFile): VirtualFile? = super.listFilesUnderProjectRoot(target.project).find { - !it.isDirectory && - it.isWritable && - it.name != target.virtualFile.name && - it.name == guessSourceFileName(target.name) - } - - /** - * check files in editors and pick one which has most substring matches to the target - */ - override fun findSourceFileByContent(target: PsiFile): VirtualFile? = searchRelevantFileInEditors(target) { myPsiFile -> - CodeWhispererClassResolver.EP_NAME.findFirstSafe { it is CodeWhispererPythonClassResolver }?.let { - val classAndMethos = it.resolveClassAndMembers(myPsiFile) - val clazz = classAndMethos[ClassResolverKey.ClassName].orEmpty() - val methods = classAndMethos[ClassResolverKey.MethodName].orEmpty() - val func = it.resolveTopLevelFunction(myPsiFile) - - clazz + methods + func - }.orEmpty() - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/SupplementalContextStrategy.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/SupplementalContextStrategy.kt deleted file mode 100644 index 17ab38d4281..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/SupplementalContextStrategy.kt +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -interface SupplementalContextStrategy - -enum class UtgStrategy : SupplementalContextStrategy { - ByName, - ByContent, - Empty, - ; - - override fun toString() = when (this) { - ByName -> "byName" - ByContent -> "byContent" - Empty -> "empty" - } -} - -enum class CrossFileStrategy : SupplementalContextStrategy { - OpenTabsBM25, - Empty, - ProjectContext, - Codemap, - ; - - override fun toString() = when (this) { - OpenTabsBM25 -> "opentabs" - Empty -> "empty" - ProjectContext -> "projectContext" - Codemap -> "codemap" - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/TypescriptCodeWhispererFileCrawler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/TypescriptCodeWhispererFileCrawler.kt deleted file mode 100644 index b126c6aa31b..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/TypescriptCodeWhispererFileCrawler.kt +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.util - -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile - -object TypescriptCodeWhispererFileCrawler : CodeWhispererFileCrawler() { - override val fileExtension: String = "ts" - override val dialects: Set = setOf("ts", "tsx") - override val testFileNamingPatterns: List = listOf( - Regex("""^(.+)\.(?i:t)est(\.ts|\.tsx)$"""), - Regex("""^(.+)\.(?i:s)pec(\.ts|\.tsx)$""") - ) - - override fun findSourceFileByName(target: PsiFile): VirtualFile? = null - - override fun findSourceFileByContent(target: PsiFile): VirtualFile? = null -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt index cbbd18990f2..eecab3fc0a6 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt @@ -11,17 +11,12 @@ import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.stub -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.generateMockCompletionDetail +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionItem +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaFileName import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaResponse import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaTestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.metadata -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.sdkHttpResponse +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testNextToken import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererActionPromoter import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.QInlineActionId.qInlineAcceptActionId import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.QInlineActionId.qInlineForceAcceptActionId @@ -37,13 +32,7 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { // Use java code to test curly braces behavior projectRule.fixture.configureByText(javaFileName, javaTestContext) - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - javaResponse - } - } + mockLspInlineCompletionResponse(javaResponse) runInEdtAndWait { projectRule.fixture.editor.caretModel.moveToOffset(projectRule.fixture.editor.document.textLength - 2) } @@ -73,8 +62,8 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { @Test fun `test accept recommendation with typeahead with matching brackets on the right`() { - val lastIndexOfNewLine = javaResponse.completions()[0].content().lastIndexOf("\n") - val recommendation = javaResponse.completions()[0].content().substring(0, lastIndexOfNewLine) + val lastIndexOfNewLine = javaResponse.items[0].insertText.lastIndexOf("\n") + val recommendation = javaResponse.items[0].insertText.substring(0, lastIndexOfNewLine) recommendation.indices.forEach { val typeahead = recommendation.substring(0, it + 1) testAcceptRecommendationWithTypingAndMatchingBracketsOnTheRight(typeahead, "(") @@ -90,20 +79,20 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { @Test fun `test accept single-line recommendation with no typeahead with partial matching brackets on the right`() { - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail("(x, y) {"), + mockLspInlineCompletionResponse( + InlineCompletionListWithReferences( + listOf( + InlineCompletionItem( + itemId = "item1", + insertText = "(x, y) {", + null, + null ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - } - } + ), + sessionId = "sessionId", + partialResultToken = testNextToken + ) + ) // any non-matching first-line right context should remain testAcceptRecommendationWithTypingAndMatchingBracketsOnTheRight("", "(", "test") @@ -160,7 +149,7 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { projectRule.fixture.editor.caretModel.moveToOffset(47) } withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + val recommendation = states.recommendationContext.details[0].completion.insertText val editor = projectRule.fixture.editor val expectedContext = buildContextWithRecommendation(recommendation + remaining) val startOffset = editor.caretModel.offset diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt index 246d0ed8c00..d8745123336 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt @@ -3,12 +3,9 @@ package software.aws.toolkits.jetbrains.services.codewhisperer -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.SystemInfo import com.intellij.testFramework.DisposableRule import com.intellij.testFramework.RuleChain -import com.intellij.testFramework.replaceService -import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -19,25 +16,19 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doReturnConsecutively import org.mockito.kotlin.mock import org.mockito.kotlin.stub -import org.mockito.kotlin.times import org.mockito.kotlin.verify import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient import software.amazon.awssdk.services.codewhispererruntime.model.ArtifactType import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisFindingsSchema import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisStatus -import software.amazon.awssdk.services.codewhispererruntime.model.CompletionType import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse -import software.amazon.awssdk.services.codewhispererruntime.model.Customization import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisRequest import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisResponse import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsRequest @@ -49,9 +40,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryE import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnalysisRequest import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnalysisResponse -import software.amazon.awssdk.services.codewhispererruntime.model.SuggestionState import software.amazon.awssdk.services.codewhispererruntime.paginators.GenerateCompletionsIterable -import software.amazon.awssdk.services.codewhispererruntime.paginators.ListAvailableCustomizationsIterable import software.amazon.awssdk.services.ssooidc.SsoOidcClient import software.aws.toolkits.core.utils.test.aString import software.aws.toolkits.jetbrains.core.MockClientManagerRule @@ -65,25 +54,12 @@ 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.sono.SONO_REGION import software.aws.toolkits.jetbrains.services.amazonq.FEATURE_EVALUATION_PRODUCT_NAME -import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.metadata -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonRequest -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponseWithToken import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.sdkHttpResponse import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptorImpl -import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization -import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.settings.AwsSettings import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import software.aws.toolkits.telemetry.CodewhispererTriggerType class CodeWhispererClientAdaptorTest { val projectRule = JavaCodeInsightTestFixtureRule() @@ -159,123 +135,6 @@ class CodeWhispererClientAdaptorTest { .isInstanceOf(CodeWhispererRuntimeClient::class.java) } - @Test - fun `listCustomizations`() { - val sdkIterable = ListAvailableCustomizationsIterable(bearerClient, ListAvailableCustomizationsRequest.builder().build()) - val mockResponse1 = ListAvailableCustomizationsResponse.builder() - .customizations( - listOf( - Customization.builder().name("custom-1").arn("arn-1").build(), - Customization.builder().name("custom-2").arn("arn-2").build() - ) - ) - .nextToken("token-1") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as ListAvailableCustomizationsResponse - - val mockResponse2 = ListAvailableCustomizationsResponse.builder() - .customizations( - listOf( - Customization.builder().name("custom-3").arn("arn-3").build(), - ) - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as ListAvailableCustomizationsResponse - - bearerClient.stub { client -> - on { client.listAvailableCustomizations(any()) } doReturnConsecutively listOf(mockResponse1, mockResponse2) - on { client.listAvailableCustomizationsPaginator(any()) } doReturn sdkIterable - } - - val actual = sut.listAvailableCustomizations(QRegionProfile("fake_profile", "fake arn")) - assertThat(actual).hasSize(3) - assertThat(actual).isEqualTo( - listOf( - CodeWhispererCustomization(name = "custom-1", arn = "arn-1", profile = QRegionProfile("fake_profile", "fake arn")), - CodeWhispererCustomization(name = "custom-2", arn = "arn-2", profile = QRegionProfile("fake_profile", "fake arn")), - CodeWhispererCustomization(name = "custom-3", arn = "arn-3", profile = QRegionProfile("fake_profile", "fake arn")) - ) - ) - } - - @Test - fun `generateCompletionsPaginator - bearer`() { - val request = pythonRequest - bearerClient.stub { client -> - on { client.generateCompletions(any()) } doReturnConsecutively listOf( - pythonResponseWithToken("first"), - pythonResponseWithToken("second"), - pythonResponseWithToken(""), - ) - } - - val nextTokens = listOf("first", "second", "") - val responses = sut.generateCompletionsPaginator(request) - - argumentCaptor().apply { - responses.forEachIndexed { i, response -> - assertThat(response.nextToken()).isEqualTo(nextTokens[i]) - response.completions().forEachIndexed { j, recommendation -> - assertThat(recommendation) - .usingRecursiveComparison() - .isEqualTo(response.completions()[j]) - } - } - verify(bearerClient, times(3)).generateCompletions(capture()) - assertThat(this.firstValue.nextToken()).isEqualTo("") - assertThat(this.secondValue.nextToken()).isEqualTo("first") - assertThat(this.thirdValue.nextToken()).isEqualTo("second") - } - } - - @Test - fun sendUserTriggerDecisionTelemetry() { - val mockModelConfiguraotr = mock { - on { activeCustomization(any()) } doReturn CodeWhispererCustomization("fake-arn", "fake-name") - } - ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfiguraotr, disposableRule.disposable) - - val file = projectRule.fixture.addFileToProject("main.java", "public class Main {}") - runInEdtAndWait { - projectRule.fixture.openFileInEditor(file.virtualFile) - } - val requestContext = CodeWhispererService.getInstance().getRequestContext( - TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), - projectRule.fixture.editor, - projectRule.project, - file, - LatencyContext(codewhispererEndToEndStart = 0, codewhispererEndToEndEnd = 20000000) - ) - - sut.sendUserTriggerDecisionTelemetry( - requestContext, - ResponseContext("fake-session-id"), - CodewhispererCompletionType.Line, - CodewhispererSuggestionState.Accept, - 3, - 1, - 2, - 10 - ) - - argumentCaptor().apply { - verify(bearerClient).sendTelemetryEvent(capture()) - firstValue.telemetryEvent().userTriggerDecisionEvent().let { - assertThat(it.completionType()).isEqualTo(CompletionType.LINE) - assertThat(it.customizationArn()).isEqualTo("fake-arn") - assertThat(it.suggestionState()).isEqualTo(SuggestionState.ACCEPT) - assertThat(it.suggestionReferenceCount()).isEqualTo(3) - assertThat(it.generatedLine()).isEqualTo(1) - assertThat(it.recommendationLatencyMilliseconds()).isEqualTo(20.0) - assertThat(it.numberOfRecommendations()).isEqualTo(2) - assertThat(it.acceptedCharacterCount()).isEqualTo(10) - } - } - } - @Test fun `createUploadUrl - bearer`() { val actual = sut.createUploadUrl(createUploadUrlRequest) @@ -327,22 +186,6 @@ class CodeWhispererClientAdaptorTest { } } - @Test - fun `sendTelemetryEvent for userTriggerDecision respects telemetry optin status, for SSO users`() { - sendTelemetryEventOptOutCheckHelper { - sut.sendUserTriggerDecisionTelemetry( - aRequestContext(projectRule.project), - aResponseContext(), - aCompletionType(), - aSuggestionState(), - 0, - 1, - 2, - 10 - ) - } - } - @Test fun `sendTelemetryEvent for codePercentage respects telemetry optin status`() { sendTelemetryEventOptOutCheckHelper { 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 deleted file mode 100644 index d181f8053b5..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt +++ /dev/null @@ -1,526 +0,0 @@ -// Copyright 2022 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.application.ApplicationManager -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Key -import com.intellij.psi.codeStyle.CodeStyleManager -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.fixtures.CodeInsightTestFixture -import com.intellij.testFramework.replaceService -import com.intellij.testFramework.runInEdtAndGet -import com.intellij.testFramework.runInEdtAndWait -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.internal.verification.Times -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.doNothing -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import software.aws.toolkits.core.telemetry.MetricEvent -import software.aws.toolkits.core.telemetry.TelemetryBatcher -import software.aws.toolkits.core.telemetry.TelemetryPublisher -import software.aws.toolkits.core.utils.test.aString -import software.aws.toolkits.jetbrains.core.MockClientManagerRule -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.keystrokeInput -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType -import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager.Companion.CODEWHISPERER_USER_ACTION_PERFORMED -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeCoverageTokens -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_SECONDS_IN_MINUTE -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy -import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher -import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService -import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import java.util.concurrent.atomic.AtomicInteger - -internal abstract class CodeWhispererCodeCoverageTrackerTestBase(myProjectRule: CodeInsightTestFixtureRule) { - protected class TestCodePercentageTracker( - project: Project, - timeWindowInSec: Long, - language: CodeWhispererProgrammingLanguage, - rangeMarkers: MutableList = mutableListOf(), - codeCoverageTokens: MutableMap = mutableMapOf(), - invocationCount: Int = 0, - ) : CodeWhispererCodeCoverageTracker(project, timeWindowInSec, language, rangeMarkers, codeCoverageTokens, AtomicInteger(invocationCount)) - - protected class TestTelemetryService( - publisher: TelemetryPublisher = NoOpPublisher(), - batcher: TelemetryBatcher, - ) : TelemetryService(publisher, batcher) - - @Rule - @JvmField - val projectRule: CodeInsightTestFixtureRule - - @Rule - @JvmField - val disposableRule = DisposableRule() - - @Rule - @JvmField - val mockClientManagerRule = MockClientManagerRule() - - protected lateinit var project: Project - protected lateinit var fixture: CodeInsightTestFixture - protected lateinit var telemetryServiceSpy: TelemetryService - protected lateinit var batcher: TelemetryBatcher - protected lateinit var exploreActionManagerMock: CodeWhispererExplorerActionManager - protected lateinit var sut: CodeWhispererCodeCoverageTracker - - init { - this.projectRule = myProjectRule - } - - open fun setup() { - this.project = projectRule.project - this.fixture = projectRule.fixture - AwsSettings.getInstance().isTelemetryEnabled = true - batcher = mock() - telemetryServiceSpy = spy(TestTelemetryService(batcher = batcher)) - - exploreActionManagerMock = mock { - on { checkActiveCodeWhispererConnectionType(any()) } doReturn CodeWhispererLoginType.Sono - } - - ApplicationManager.getApplication().replaceService(CodeWhispererExplorerActionManager::class.java, exploreActionManagerMock, disposableRule.disposable) - ApplicationManager.getApplication().replaceService(TelemetryService::class.java, telemetryServiceSpy, disposableRule.disposable) - } - - @After - fun tearDown() { - CodeWhispererCodeCoverageTracker.getInstancesMap().clear() - if (::sut.isInitialized) { - sut.forceTrackerFlush() - } - } - - protected companion object { - const val CODE_PERCENTAGE = "codewhisperer_codePercentage" - const val CWSPR_PERCENTAGE = "codewhispererPercentage" - const val CWSPR_Language = "codewhispererLanguage" - const val CWSPR_ACCEPTED_TOKENS = "codewhispererAcceptedTokens" - const val CWSPR_TOTAL_TOKENS = "codewhispererTotalTokens" - const val CWSPR_RAW_ACCEPTED_TOKENS = "codewhispererSuggestedTokens" - } -} - -internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCoverageTrackerTestBase(PythonCodeInsightTestFixtureRule()) { - private lateinit var invocationContext: InvocationContext - private lateinit var sessionContext: SessionContext - - @Before - override fun setup() { - super.setup() - fixture.configureByText(pythonFileName, pythonTestLeftContext) - runInEdtAndWait { - projectRule.fixture.editor.caretModel.primaryCaret.moveToOffset(projectRule.fixture.editor.document.textLength) - } - - val requestContext = RequestContext( - project, - fixture.editor, - mock(), - mock(), - FileContextInfo(mock(), pythonFileName, CodeWhispererPython.INSTANCE, pythonFileName, null), - runBlocking { - async { - SupplementalContextInfo( - isUtg = false, - contents = emptyList(), - targetFileName = "", - strategy = CrossFileStrategy.Empty, - latency = 0L - ) - } - }, - null, - mock(), - aString(), - aString(), - aString(), - emptyList() - ) - val responseContext = ResponseContext("sessionId") - val recommendationContext = RecommendationContext( - listOf( - DetailContext( - "requestId", - pythonResponse.completions()[0], - pythonResponse.completions()[0], - false, - false, - "", - CodewhispererCompletionType.Block - ) - ), - "x, y", - "x, y", - mock() - ) - invocationContext = InvocationContext(requestContext, responseContext, recommendationContext, mock()) - sessionContext = SessionContext() - - // it is needed because referenceManager is listening to CODEWHISPERER_USER_ACTION_PERFORMED topic - project.replaceService(CodeWhispererCodeReferenceManager::class.java, mock(), disposableRule.disposable) - } - - @Test - fun `test getInstance()`() { - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(0) - val javaInstance = CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererJava.INSTANCE) - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(1) - assertThat(javaInstance).isNotNull - - val javaInstance2 = CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererJava.INSTANCE) - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(1) - assertThat(javaInstance == javaInstance2).isTrue - - val pythonInstance = CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE) - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(2) - val pythonInstance2 = CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE) - assertThat(pythonInstance == pythonInstance2).isTrue - - CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererJavaScript.INSTANCE) - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(3) - } - - @Test - fun `test tracker is listening to codewhisperer recommendation service invocation`() { - sut = TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE) - val jsxTracker = TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererJsx.INSTANCE) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererJsx.INSTANCE] = jsxTracker - sut.activateTrackerIfNotActive() - assertThat(sut.serviceInvocationCount).isEqualTo(0) - assertThat(jsxTracker.serviceInvocationCount).isEqualTo(0) - - val fileContextInfo = mock { on { programmingLanguage } doReturn CodeWhispererPython.INSTANCE } - ApplicationManager.getApplication().messageBus.syncPublisher(CodeWhispererService.CODEWHISPERER_CODE_COMPLETION_PERFORMED).onSuccess(fileContextInfo) - assertThat(sut.serviceInvocationCount).isEqualTo(1) - assertThat(jsxTracker.serviceInvocationCount).isEqualTo(0) - } - - @Test - fun `test tracker is listening to document changes and increment totalTokens - add new code`() { - sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE)) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut - sut.activateTrackerIfNotActive() - - fixture.configureByText(pythonFileName, "") - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(keystrokeInput) - } - } - val captor = argumentCaptor() - verify(sut, Times(1)).documentChanged(captor.capture()) - assertThat(captor.firstValue.newFragment.toString()).isEqualTo(keystrokeInput) - assertThat(sut.totalCharsCount).isEqualTo(keystrokeInput.length.toLong()) - } - - @Test - fun `test tracker is not listening to multi char input more than 50 and will not increment totalTokens - add new code`() { - sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE)) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut - sut.activateTrackerIfNotActive() - - fixture.configureByText(pythonFileName, "") - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(pythonTestLeftContext) - } - } - val captor = argumentCaptor() - verify(sut, Times(1)).documentChanged(captor.capture()) - assertThat(captor.firstValue.newFragment.toString()).isEqualTo(pythonTestLeftContext) - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length.toLong()) - - val anotherCode = "(x, y):".repeat(8) - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(anotherCode) - } - } - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length.toLong()) - } - - @Test - fun `test tracker is listening to document changes and increment totalTokens - delete code should not affect`() { - sut = TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - CodeWhispererPython.INSTANCE, - codeCoverageTokens = mutableMapOf(fixture.editor.document to CodeCoverageTokens(pythonTestLeftContext.length, 0)) - ) - - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut - sut.activateTrackerIfNotActive() - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length.toLong()) - - runInEdtAndWait { - fixture.editor.caretModel.primaryCaret.moveToOffset(fixture.editor.document.textLength) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.document.deleteString(fixture.editor.caretModel.offset - 3, fixture.editor.caretModel.offset) - } - } - - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length.toLong()) - } - - @Test - fun `test tracker documentChanged - will increment tokens on user input blank string`() { - sut = TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - CodeWhispererPython.INSTANCE, - codeCoverageTokens = mutableMapOf(fixture.editor.document to CodeCoverageTokens(pythonTestLeftContext.length, 0)) - ) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut - sut.activateTrackerIfNotActive() - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length.toLong()) - - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.document.insertString(fixture.editor.caretModel.offset, "\t") - } - } - - assertThat(sut.totalCharsCount).isEqualTo(pythonTestLeftContext.length + 1L) - } - - @Test - fun `test msg CODEWHISPERER_USER_ACTION_PERFORMED will add rangeMarker in the list`() { - sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, language = CodeWhispererPython.INSTANCE)) - sut.activateTrackerIfNotActive() - val rangeMarkerMock = runInEdtAndGet { - spy(fixture.editor.document.createRangeMarker(0, 3)) { - on { isValid } doReturn true - } - } - - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_USER_ACTION_PERFORMED).afterAccept( - invocationContext, - mock(), - rangeMarkerMock - ) - - assertThat(sut.acceptedRecommendationsCount).isEqualTo(1) - val argumentCaptor = argumentCaptor() - verify(rangeMarkerMock, atLeastOnce()).putUserData(any>(), argumentCaptor.capture()) - assertThat(argumentCaptor.firstValue).isEqualTo(pythonTestLeftContext.substring(0, 3)) - } - - @Test - fun `test 0 totalTokens will return null`() { - sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, language = CodeWhispererJava.INSTANCE)) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererJava.INSTANCE] = sut - assertThat(sut.percentage).isNull() - } - - @Test - fun `test flush() will reset tokens and reschedule next telemetry sending`() { - sut = TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - CodeWhispererPython.INSTANCE, - codeCoverageTokens = mutableMapOf(mock() to CodeCoverageTokens("foobar".length, "bar".length)) - ) - - sut.activateTrackerIfNotActive() - assertThat(sut.activeRequestCount()).isEqualTo(1) - assertThat(sut.unmodifiedAcceptedCharsCount).isEqualTo("bar".length.toLong()) - assertThat(sut.totalCharsCount).isEqualTo("foobar".length.toLong()) - - sut.forceTrackerFlush() - - assertThat(sut.activeRequestCount()).isEqualTo(1) - assertThat(sut.unmodifiedAcceptedCharsCount).isEqualTo(0) - assertThat(sut.totalCharsCount).isEqualTo(0) - } - - @Test - fun `test flush() will call emitTelemetry automatically schedule next call`() { - sut = spy( - TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - CodeWhispererPython.INSTANCE, - ) - ) - doNothing().whenever(sut).emitCodeWhispererCodeContribution() - - sut.activateTrackerIfNotActive() - assertThat(sut.activeRequestCount()).isEqualTo(1) - sut.forceTrackerFlush() - - verify(sut, Times(1)).emitCodeWhispererCodeContribution() - assertThat(sut.activeRequestCount()).isEqualTo(1) - } - - @Test - fun `test emitCodeWhispererCodeContribution`() { - val rangeMarkerMock1 = mock { - on { isValid } doReturn true - on { getUserData(any>()) } doReturn "foo" - on { document } doReturn fixture.editor.document - } - sut = spy( - TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - CodeWhispererPython.INSTANCE, - mutableListOf(rangeMarkerMock1), - mutableMapOf(fixture.editor.document to CodeCoverageTokens(totalChars = 100, unmodifiedAcceptedChars = 0, acceptedChars = 0)), - 1 - ) - ) { - onGeneric { extractRangeMarkerString(any()) } doReturn "fou" - } - sut.emitCodeWhispererCodeContribution() - - val metricCaptor = argumentCaptor() - verify(batcher, Times(1)).enqueue(metricCaptor.capture()) - CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - CODE_PERCENTAGE, - 1, - CWSPR_ACCEPTED_TOKENS to "2", - CWSPR_RAW_ACCEPTED_TOKENS to "3", - CWSPR_TOTAL_TOKENS to "100", - ) - } - - @Test - fun `test flush() won't emit telemetry event when users not enabling telemetry`() { - AwsSettings.getInstance().isTelemetryEnabled = false - sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE)) - doNothing().whenever(sut).emitCodeWhispererCodeContribution() - - sut.activateTrackerIfNotActive() - sut.forceTrackerFlush() - - verify(sut, Times(0)).emitCodeWhispererCodeContribution() - } - - @Test - fun `test flush() won't emit telemetry when users are not editing the document (totalTokens == 0)`() { - sut = TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE) - sut.activateTrackerIfNotActive() - assertThat(sut.activeRequestCount()).isEqualTo(1) - sut.forceTrackerFlush() - verify(batcher, Times(0)).enqueue(any()) - } - - private fun Editor.appendString(string: String) { - val currentOffset = caretModel.primaryCaret.offset - document.insertString(currentOffset, string) - caretModel.moveToOffset(currentOffset + string.length) - } -} - -internal class CodeWhispererCodeCoverageTrackerTestJava : CodeWhispererCodeCoverageTrackerTestBase(JavaCodeInsightTestFixtureRule()) { - @Before - override fun setup() { - super.setup() - } - - @Test - fun `tracker should not update totalTokens if documentChanged events are fired by code reformatting`() { - val codeNeedToBeReformatted = """ - class Answer { - private int knapsack(int[] w, int[] v, int c) { - int[][] dp = new int[w.length + 1][c + 1]; - for (int i = 0; i < w.length; i++) { - for (int j = 0; j <= c; j++) { - if (j < w[i]) { - dp[i + 1][j] = dp[i][j]; - } - else { - dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - w[i]] + v[i]); - } - } - } - return dp[w.length][c]; - } - } - """.trimIndent() - val file = fixture.configureByText("test.java", codeNeedToBeReformatted) - sut = spy( - TestCodePercentageTracker( - project, - TOTAL_SECONDS_IN_MINUTE, - language = CodeWhispererJava.INSTANCE, - codeCoverageTokens = mutableMapOf(fixture.editor.document to CodeCoverageTokens(totalChars = codeNeedToBeReformatted.length)) - ) - ) - CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererJava.INSTANCE] = sut - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - CodeStyleManager.getInstance(project).reformatText(file, 0, fixture.editor.document.textLength) - } - } - // reformat should fire documentChanged events, but tracker should not update token from these events - verify(sut, atLeastOnce()).documentChanged(any()) - assertThat(sut.totalCharsCount).isEqualTo(codeNeedToBeReformatted.length.toLong()) - - val formatted = """ - class Answer { - private int knapsack(int[] w, int[] v, int c) { - int[][] dp = new int[w.length + 1][c + 1]; - for (int i = 0; i < w.length; i++) { - for (int j = 0; j <= c; j++) { - if (j < w[i]) { - dp[i + 1][j] = dp[i][j]; - } else { - dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - w[i]] + v[i]); - } - } - } - return dp[w.length][c]; - } - } - """.trimIndent() - assertThat(fixture.editor.document.text.trimEnd()).isEqualTo(formatted) - } -} 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 67aff7a14e3..cd4784aa612 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 @@ -4,14 +4,11 @@ package software.aws.toolkits.jetbrains.services.codewhisperer import com.intellij.openapi.application.ApplicationManager -import com.intellij.testFramework.replaceService import com.intellij.ui.dsl.builder.components.DslLabel import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import org.mockito.Mockito import org.mockito.kotlin.doNothing import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener -import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable import software.aws.toolkits.resources.message import javax.swing.JCheckBox @@ -22,11 +19,9 @@ class CodeWhispererConfigurableTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer configurable`() { - val codeScanManagerSpy = Mockito.spy(CodeWhispererCodeScanManager.getInstance(projectRule.project)) - doNothing().`when`(codeScanManagerSpy).buildCodeScanUI() - doNothing().`when`(codeScanManagerSpy).showCodeScanUI() - doNothing().`when`(codeScanManagerSpy).removeCodeScanUI() - projectRule.project.replaceService(CodeWhispererCodeScanManager::class.java, codeScanManagerSpy, disposableRule.disposable) + doNothing().`when`(codeScanManager).buildCodeScanUI() + doNothing().`when`(codeScanManager).showCodeScanUI() + doNothing().`when`(codeScanManager).removeCodeScanUI() val configurable = CodeWhispererConfigurable(projectRule.project) // A workaround to initialize disposable in the DslConfigurableBase since somehow the disposable is diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileContextProviderTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileContextProviderTest.kt index 588aa7c844b..61ed40d41d8 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileContextProviderTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileContextProviderTest.kt @@ -4,63 +4,21 @@ package software.aws.toolkits.jetbrains.services.codewhisperer import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.readAction import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile import com.intellij.testFramework.DisposableRule import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture import com.intellij.testFramework.replaceService import com.intellij.testFramework.runInEdtAndGet -import com.intellij.testFramework.runInEdtAndWait -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mockito.mockConstruction -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.stub -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import software.aws.toolkits.core.utils.test.aStringWithLineCount import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService -import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer -import software.aws.toolkits.jetbrains.services.amazonq.project.InlineBm25Chunk -import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController -import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCpp -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCsharp -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererGo import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererKotlin -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererRuby -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript -import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk -import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy import software.aws.toolkits.jetbrains.services.codewhisperer.util.DefaultCodeWhispererFileContextProvider import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider import software.aws.toolkits.jetbrains.utils.rules.HeavyJavaCodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.addClass -import software.aws.toolkits.jetbrains.utils.rules.addModule -import software.aws.toolkits.jetbrains.utils.rules.addTestClass -import kotlin.test.assertNotNull class CodeWhispererFileContextProviderTest { @JvmField @@ -75,7 +33,6 @@ class CodeWhispererFileContextProviderTest { // dependencies lateinit var featureConfigService: CodeWhispererFeatureConfigService - lateinit var mockProjectContext: ProjectContextController lateinit var fixture: JavaCodeInsightTestFixture lateinit var project: Project @@ -94,246 +51,6 @@ class CodeWhispererFileContextProviderTest { featureConfigService, disposableRule.disposable ) - - mockProjectContext = mock() - project.replaceService(ProjectContextController::class.java, mockProjectContext, disposableRule.disposable) - } - - @Test - fun `generateQuery should use last 50 lines (excluding the current line) of left context`() { - val fileContext = FileContextInfo( - CaretContext( - leftFileContext = aStringWithLineCount(100), - rightFileContext = "", - leftContextOnCurrentLine = "" - ), - "Foo.java", - CodeWhispererJava.INSTANCE, - "", - "" - ) - - val actual = sut.generateQuery(fileContext) - val expected = aStringWithLineCount(lineCount = 50, start = 49) - assertThat(actual).isEqualTo(expected) - assertThat(actual.split("\n").size).isEqualTo(expected.split("\n").size) - } - - @Test - fun `generateQuery should use last 50 lines (excluding the current line) of left context if current caret is at the beginning of the line`() { - val fileContext = FileContextInfo( - CaretContext( - leftFileContext = aStringWithLineCount(100) + "\n", - rightFileContext = "", - leftContextOnCurrentLine = "" - ), - "Foo.java", - CodeWhispererJava.INSTANCE, - "", - "" - ) - - val actual = sut.generateQuery(fileContext) - val expected = aStringWithLineCount(lineCount = 50, start = 50) - assertThat(actual).isEqualTo(expected) - assertThat(actual.split("\n").size).isEqualTo(expected.split("\n").size) - } - - @Test - fun `extractSupplementalFileContext should timeout 50ms`() = runTest { - mockProjectContext.stub { onBlocking { queryInline(any(), any()) }.doReturn(emptyList()) } - sut = spy(sut) - - val files = NaiveSampleCase.setupFixture(fixture) - val queryPsi = files[0] - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - - sut.stub { - runBlocking { - doAnswer { - runBlocking { delay(100) } - aSupplementalContextInfo() - }.whenever(sut).fetchOpenTabsContext(any(), any(), any()) - } - } - - val result = sut.extractSupplementalFileContext(queryPsi, mockFileContext, 50L) - assertNotNull(result) - assertThat(result.isProcessTimeout).isTrue - } - - @Test - fun `should return empty if both project context and opentabs context return empty`() = runTest { - sut = spy(sut) - - mockProjectContext.stub { onBlocking { queryInline(any(), any()) }.doReturn(emptyList()) } - val queryPsi = projectRule.fixture.addFileToProject("Foo.java", "public Foo {}") - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - - val result = sut.extractSupplementalFileContextForSrc(queryPsi, mockFileContext) - - verify(sut, times(1)).fetchProjectContext(any(), any(), any()) - verify(sut, times(1)).fetchOpenTabsContext(any(), any(), any()) - - assertThat(result.isUtg).isFalse - assertThat(result.strategy).isEqualTo(CrossFileStrategy.Empty) - assertThat(result.contents).isEmpty() - } - - @Test - fun `should only use openTabsContext if projectContext is empty`() = runTest { - mockProjectContext.stub { onBlocking { queryInline(any(), any()) }.doReturn(emptyList()) } - featureConfigService.stub { on { getInlineCompletion() } doReturn false } - sut = spy(sut) - - val files = NaiveSampleCase.setupFixture(fixture) - val queryPsi = files[0] - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - - val result = sut.extractSupplementalFileContextForSrc(queryPsi, mockFileContext) - - verify(sut, times(1)).fetchProjectContext(any(), any(), any()) - verify(sut, times(1)).fetchOpenTabsContext(any(), any(), any()) - - assertThat(result.isUtg).isFalse - assertThat(result.strategy).isEqualTo(CrossFileStrategy.OpenTabsBM25) - assertThat(result.contents).isNotEmpty - } - - @Test - fun `should call both and use openTabsContext if projectContext is empty when it's enabled`() = runTest { - mockProjectContext.stub { onBlocking { queryInline(any(), any()) }.doReturn(emptyList()) } - featureConfigService.stub { on { getInlineCompletion() } doReturn true } - sut = spy(sut) - - val files = NaiveSampleCase.setupFixture(fixture) - val queryPsi = files[0] - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - - val result = sut.extractSupplementalFileContextForSrc(queryPsi, mockFileContext) - - verify(sut, times(1)).fetchProjectContext(any(), any(), any()) - verify(sut, times(1)).fetchOpenTabsContext(any(), any(), any()) - - assertThat(result.isUtg).isFalse - assertThat(result.strategy).isEqualTo(CrossFileStrategy.OpenTabsBM25) - assertThat(result.contents).isNotEmpty - } - - // move to projectContextControllerTest - @Test - fun `projectContextController should return empty result if provider throws`() = runTest { - mockConstruction(ProjectContextProvider::class.java).use { providerContext -> - mockConstruction(EncoderServer::class.java).use { serverContext -> - assertThat(providerContext.constructed()).hasSize(0) - assertThat(serverContext.constructed()).hasSize(0) - val controller = ProjectContextController(project, TestScope()) - assertThat(providerContext.constructed()).hasSize(1) - assertThat(serverContext.constructed()).hasSize(1) - - whenever(providerContext.constructed()[0].queryInline(any(), any(), any())).thenThrow(RuntimeException("mock exception")) - - val result = controller.queryInline("query", "filePath") - assertThat(result).isEmpty() - } - } - } - - @Test - fun `should use both project context and open tabs if both are present`() = runTest { - mockProjectContext.stub { - runBlocking { - doReturn( - listOf( - InlineBm25Chunk("project_context1", "path1", 0.0), - ) - ).whenever(it).queryInline(any(), any()) - } - } - sut = spy(sut) - val files = NaiveSampleCase.setupFixture(fixture) - val queryPsi = files[0] - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - - val result = sut.extractSupplementalFileContextForSrc(queryPsi, mockFileContext) - - assertThat(result.isUtg).isFalse - assertThat(result.strategy).isEqualTo(CrossFileStrategy.Codemap) - assertThat(result.contents).hasSize(4) - } - - @Test - fun `crossfile configuration`() { - assertThat(CodeWhispererConstants.CrossFile.CHUNK_SIZE).isEqualTo(60) - } - - @Test - fun `shouldFetchUtgContext - fully support`() { - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererJava.INSTANCE)).isTrue - } - - @Test - fun `shouldFetchUtgContext - no support`() { - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererPython.INSTANCE)).isFalse - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererJavaScript.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererJsx.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererTypeScript.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererTsx.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererCsharp.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererKotlin.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererGo.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchUtgContext(CodeWhispererTsx.INSTANCE)).isNull() - } - - @Test - fun `shouldFetchCrossfileContext - fully support`() { - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererJava.INSTANCE)).isTrue - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererPython.INSTANCE)).isTrue - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererJavaScript.INSTANCE)).isTrue - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererJsx.INSTANCE)).isTrue - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererTypeScript.INSTANCE)).isTrue - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererTsx.INSTANCE)).isTrue - } - - @Test - fun `shouldFetchCrossfileContext - no support`() { - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererCsharp.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererKotlin.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererGo.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererCpp.INSTANCE)).isNull() - - assertThat(DefaultCodeWhispererFileContextProvider.shouldFetchCrossfileContext(CodeWhispererRuby.INSTANCE)).isNull() - } - - @Test - fun `languages not supporting supplemental context will return empty`() = runTest { - val psiFiles = setupFixture(fixture) - val psi = psiFiles[0] - - var context = aFileContextInfo(CodeWhispererCsharp.INSTANCE) - - assertThat(sut.extractSupplementalFileContextForSrc(psi, context).contents).isEmpty() - assertThat(sut.extractSupplementalFileContextForTst(psi, context).contents).isEmpty() - - context = aFileContextInfo(CodeWhispererKotlin.INSTANCE) - assertThat(sut.extractSupplementalFileContextForSrc(psi, context).contents).isEmpty() - assertThat(sut.extractSupplementalFileContextForTst(psi, context).contents).isEmpty() } @Test @@ -386,304 +103,4 @@ class CodeWhispererFileContextProviderTest { ) assertThat(fileContext.caretContext.leftContextOnCurrentLine).isEqualTo(" public static void main") } - - @Test - fun `test extractCodeChunksFromFiles should read files from file producers to get 60 chunks`() = runTest { - val psiFiles = setupFixture(fixture) - val virtualFiles = psiFiles.mapNotNull { it.virtualFile } - val javaMainPsiFile = psiFiles.first() - - val fileProducer1: suspend (PsiFile) -> List = { psiFile -> - listOf(virtualFiles[1]) - } - - val fileProducer2: suspend (PsiFile) -> List = { psiFile -> - listOf(virtualFiles[2]) - } - - val result = sut.extractCodeChunksFromFiles(javaMainPsiFile, listOf(fileProducer1, fileProducer2)) - - assertThat(result[0].content).isEqualTo( - """public class UtilClass { - | public static int util() {} - | public static String util2() {} - """.trimMargin() - ) - - assertThat(result[1].content).isEqualTo( - """public class UtilClass { - | public static int util() {} - | public static String util2() {} - | private static void helper() {} - | public static final int constant1; - | public static final int constant2; - | public static final int constant3; - |} - """.trimMargin() - ) - - assertThat(result[2].content).isEqualTo( - """public class MyController { - | @Get - | public Response getRecommendation(Request req) {} - """.trimMargin() - ) - - assertThat(result[3].content).isEqualTo( - """public class MyController { - | @Get - | public Response getRecommendation(Request req) {} - |} - """.trimMargin() - ) - } - - @Test - fun `extractSupplementalFileContext should return opentabs context if project context is empty`() = runTest { - mockProjectContext.stub { onBlocking { queryInline(any(), any()) }.doReturn(emptyList()) } - val files = NaiveSampleCase.setupFixture(fixture) - val queryPsi = files[0] - - sut = spy(sut) - - val fileContext = readAction { sut.extractFileContext(fixture.editor, queryPsi) } - val supplementalContext = sut.extractSupplementalFileContext(queryPsi, fileContext, timeout = 50) - - assertThat(supplementalContext?.contents) - .isNotNull - .isNotEmpty - .hasSize(3) - - assertThat(supplementalContext?.contents) - .isNotNull - .isNotEmpty - - assertThat(supplementalContext?.strategy) - .isNotNull - .isEqualTo(CrossFileStrategy.OpenTabsBM25) - verify(sut).extractSupplementalFileContextForSrc(any(), any()) - verify(sut, times(0)).extractSupplementalFileContextForTst(any(), any()) - } - - /** - * - src/ - * - java/ - * - Main.java - * - Util.java - * - controllers/ - * -MyApiController.java - * - tst/ - * - java/ - * - MainTest.java - * - */ - @Test - fun `extractSupplementalFileContext from tst file should extract focal file`() = runTest { - val module = fixture.addModule("main") - fixture.addClass(module, JAVA_MAIN) - - val psiTestClass = fixture.addTestClass( - module, - """ - public class MainTest {} - """ - ) - - val tstFile = psiTestClass.containingFile - - sut = spy(sut) - - val fileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - val supplementalContext = sut.extractSupplementalFileContext(tstFile, fileContext, 50) - - assertThat(supplementalContext?.contents) - .isNotNull - .isNotEmpty - .hasSize(1) - - assertThat(supplementalContext?.contents?.get(0)?.content) - .isNotNull - .isEqualTo("UTG\n$JAVA_MAIN") - - verify(sut, times(0)).extractSupplementalFileContextForSrc(any(), any()) - verify(sut).extractSupplementalFileContextForTst(any(), any()) - } - - @Test - fun `truncate context should make context length fit in 20480 cap`() { - val supplementalContext = SupplementalContextInfo( - isUtg = false, - contents = listOf( - Chunk(content = "a".repeat(10000), path = "a.java"), - Chunk(content = "b".repeat(10000), path = "b.java"), - Chunk(content = "c".repeat(10000), path = "c.java"), - Chunk(content = "d".repeat(10000), path = "d.java"), - Chunk(content = "e".repeat(10000), path = "e.java"), - ), - targetFileName = "foo", - strategy = CrossFileStrategy.Codemap - ) - - val r = sut.truncateContext(supplementalContext) - assertThat(r.contents).hasSize(2) - assertThat(r.contentLength).isEqualTo(20000) - assertThat(r.strategy).isEqualTo(CrossFileStrategy.Codemap) - assertThat(r.targetFileName).isEqualTo("foo") - } - - @Test - fun `truncate context should make context item lte 5`() { - val supplementalContext = SupplementalContextInfo( - isUtg = false, - contents = listOf( - Chunk(content = "a", path = "a.java"), - Chunk(content = "b", path = "b.java"), - Chunk(content = "c", path = "c.java"), - Chunk(content = "d", path = "d.java"), - Chunk(content = "e", path = "e.java"), - Chunk(content = "f", path = "e.java"), - Chunk(content = "g", path = "e.java"), - ), - targetFileName = "foo", - strategy = CrossFileStrategy.Codemap - ) - - val r = sut.truncateContext(supplementalContext) - assertThat(r.contents).hasSize(5) - assertThat(r.strategy).isEqualTo(CrossFileStrategy.Codemap) - assertThat(r.targetFileName).isEqualTo("foo") - } - - @Test - fun `truncate context should make context length per item fit in 10240 cap`() { - val chunkA = Chunk(content = "a\n".repeat(4000), path = "a.java") - val chunkB = Chunk(content = "b\n".repeat(6000), path = "b.java") - val chunkC = Chunk(content = "c\n".repeat(1000), path = "c.java") - val chunkD = Chunk(content = "d\n".repeat(1500), path = "d.java") - - assertThat(chunkA.content.length).isEqualTo(8000) - assertThat(chunkB.content.length).isEqualTo(12000) - assertThat(chunkC.content.length).isEqualTo(2000) - assertThat(chunkD.content.length).isEqualTo(3000) - assertThat(chunkA.content.length + chunkB.content.length + chunkC.content.length + chunkD.content.length).isEqualTo(25000) - - val supplementalContext = SupplementalContextInfo( - isUtg = false, - contents = listOf( - chunkA, - chunkB, - chunkC, - chunkD, - ), - targetFileName = "foo", - strategy = CrossFileStrategy.Codemap - ) - - val r = sut.truncateContext(supplementalContext) - - assertThat(r.contents).hasSize(3) - val truncatedChunkA = r.contents[0] - val truncatedChunkB = r.contents[1] - val truncatedChunkC = r.contents[2] - - assertThat(truncatedChunkA.content.length).isEqualTo(8000) - assertThat(truncatedChunkB.content.length).isEqualTo(10240) - assertThat(truncatedChunkC.content.length).isEqualTo(2000) - - assertThat(r.contentLength).isEqualTo(20240) - assertThat(r.strategy).isEqualTo(CrossFileStrategy.Codemap) - assertThat(r.targetFileName).isEqualTo("foo") - } - - private fun setupFixture(fixture: JavaCodeInsightTestFixture): List { - val psiFile1 = fixture.addFileToProject("Main.java", JAVA_MAIN) - val psiFile2 = fixture.addFileToProject("UtilClass.java", JAVA_UTILCLASS) - val psiFile3 = fixture.addFileToProject("controllers/MyController.java", JAVA_MYCONTROLLER) - val psiFile4 = fixture.addFileToProject("helpers/Helper1.java", "Class Helper1 {}") - val psiFile5 = fixture.addFileToProject("helpers/Helper2.java", "Class Helper2 {}") - val psiFile6 = fixture.addFileToProject("helpers/Helper3.java", "Class Helper3 {}") - val testPsiFile = fixture.addFileToProject( - "test/java/MainTest.java", - """ - public class MainTest { - @Before - public void setup() {} - } - """.trimIndent() - ) - - runInEdtAndWait { - fixture.openFileInEditor(psiFile1.virtualFile) - fixture.editor.caretModel.moveToOffset(fixture.editor.document.textLength) - } - - return listOf(psiFile1, psiFile2, psiFile3, testPsiFile, psiFile4, psiFile5, psiFile6) - } - - companion object { - private val JAVA_MAIN = """public class Main { - | public static void main(String[] args) { - | System.out.println("Hello world"); - | } - |} - """.trimMargin() - - // language=Java - private val JAVA_UTILCLASS = """public class UtilClass { - | public static int util() {} - | public static String util2() {} - | private static void helper() {} - | public static final int constant1; - | public static final int constant2; - | public static final int constant3; - |} - """.trimMargin() - - // language=Java - private val JAVA_MYCONTROLLER = """public class MyController { - | @Get - | public Response getRecommendation(Request req) {} - |} - """.trimMargin() - } -} - -private object NaiveSampleCase { - const val file1 = "Human machine interface for lab abc computer applications" - const val file2 = "A survey of user opinion of computer system response time" - const val file3 = "The EPS user interface management system" - const val file4 = "System and human system engineering testing of EPS" - const val file5 = "Relation of user perceived response time to error measurement" - const val file6 = "The generation of random binary unordered trees" - const val file7 = "The intersection graph of paths in trees" - const val file8 = "Graph minors IV Widths of trees and well quasi ordering" - const val file9 = "Graph minors A survey" - const val query = "The intersection of graph survey and trees" - - fun setupFixture(fixture: JavaCodeInsightTestFixture): List { - val queryPsi = fixture.addFileToProject("Query.java", query) - val file1Psi = fixture.addFileToProject("File1.java", file1) - val file2Psi = fixture.addFileToProject("File2.java", file2) - val file3Psi = fixture.addFileToProject("File3.java", file3) - val file4Psi = fixture.addFileToProject("File4.java", file4) - val file5Psi = fixture.addFileToProject("File5.java", file5) - val file6Psi = fixture.addFileToProject("File6.java", file6) - val file7Psi = fixture.addFileToProject("File7.java", file7) - val file8Psi = fixture.addFileToProject("File8.java", file8) - val file9Psi = fixture.addFileToProject("File9.java", file9) - - runInEdtAndWait { - fixture.openFileInEditor(file1Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file2Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file3Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file4Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file5Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file6Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file7Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file8Psi.viewProvider.virtualFile) - fixture.openFileInEditor(file9Psi.viewProvider.virtualFile) - } - - return listOf(queryPsi, file1Psi, file2Psi, file3Psi, file4Psi, file5Psi, file6Psi, file7Psi, file8Psi, file9Psi) - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileCrawlerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileCrawlerTest.kt deleted file mode 100644 index 9f56d862a7c..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFileCrawlerTest.kt +++ /dev/null @@ -1,652 +0,0 @@ -// Copyright 2023 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.application.runReadAction -import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.ExtensionTestUtil -import com.intellij.testFramework.fixtures.CodeInsightTestFixture -import com.intellij.testFramework.runInEdtAndWait -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import software.aws.toolkits.core.utils.test.aString -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispereJavaClassResolver -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispererClassResolver -import software.aws.toolkits.jetbrains.services.codewhisperer.language.classresolver.CodeWhispererPythonClassResolver -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererFileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.JavaCodeWhispererFileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.JavascriptCodeWhispererFileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.PythonCodeWhispererFileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.TypescriptCodeWhispererFileCrawler -import software.aws.toolkits.jetbrains.services.codewhisperer.util.UtgStrategy -import software.aws.toolkits.jetbrains.services.codewhisperer.util.content -import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule - -// TODO: Make different language file crawler different files and move to language/ folder -class CodeWhispererFileCrawlerTest { - @JvmField - @Rule - val projectRule: CodeInsightTestFixtureRule = CodeInsightTestFixtureRule() - - lateinit var sut: CodeWhispererFileCrawler - - lateinit var fixture: CodeInsightTestFixture - lateinit var project: Project - - @Before - fun setup() { - fixture = projectRule.fixture - project = projectRule.project - } - - @Test - fun `searchRelevantFileInEditors should exclude target file itself and files with different file extension`() { - val targetFile = fixture.addFileToProject("Foo.java", "I have 10 Foo in total, Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo") - - val file0 = fixture.addFileToProject("file0.py", "I have 7 Foo, Foo, Foo, Foo, Foo, Foo, Foo, but I am a pyfile") - val file1 = fixture.addFileToProject("File1.java", "I have 4 Foo key words : Foo, Foo, Foo") - val file2 = fixture.addFileToProject("File2.java", "I have 2 Foo Foo") - val file3 = fixture.addFileToProject("File3.java", "I have only 1 Foo") - val file4 = fixture.addFileToProject("File4.java", "bar bar bar, i have a lot of bar") - - runInEdtAndWait { - fixture.openFileInEditor(targetFile.virtualFile) - fixture.openFileInEditor(file0.virtualFile) - fixture.openFileInEditor(file1.virtualFile) - fixture.openFileInEditor(file2.virtualFile) - fixture.openFileInEditor(file3.virtualFile) - fixture.openFileInEditor(file4.virtualFile) - } - - listOf( - JavaCodeWhispererFileCrawler, - PythonCodeWhispererFileCrawler, - TypescriptCodeWhispererFileCrawler, - JavascriptCodeWhispererFileCrawler - ).forEach { - sut = it - - val result = CodeWhispererFileCrawler.searchRelevantFileInEditors(targetFile) { psiFile -> - psiFile.virtualFile.content().split(" ") - } - assertThat(result).isEqualTo(file1.virtualFile) - } - } - - @Test - fun `searchKeywordsInOpenedFile is language agnostic`() { - sut = JavaCodeWhispererFileCrawler - - val targetFile = fixture.addFileToProject("Foo.ts", "I have 10 Foo in total, Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo") - - val file0 = fixture.addFileToProject("file0.java", "I have 7 Foo, Foo, Foo, Foo, Foo, Foo, Foo, but I am a pyfile") - val file1 = fixture.addFileToProject("File1.ts", "I have 4 Foo key words : Foo, Foo, Foo") - val file2 = fixture.addFileToProject("File2.ts", "I have 2 Foo Foo") - val file3 = fixture.addFileToProject("File3.ts", "I have only 1 Foo") - val file4 = fixture.addFileToProject("File4.ts", "bar bar bar, i have a lot of bar") - - runInEdtAndWait { - fixture.openFileInEditor(targetFile.virtualFile) - fixture.openFileInEditor(file0.virtualFile) - fixture.openFileInEditor(file1.virtualFile) - fixture.openFileInEditor(file2.virtualFile) - fixture.openFileInEditor(file3.virtualFile) - fixture.openFileInEditor(file4.virtualFile) - } - - listOf( - JavaCodeWhispererFileCrawler, - PythonCodeWhispererFileCrawler, - TypescriptCodeWhispererFileCrawler, - JavascriptCodeWhispererFileCrawler - ).forEach { - sut = it - - val result = CodeWhispererFileCrawler.searchRelevantFileInEditors(targetFile) { psiFile -> - psiFile.virtualFile.content().split(" ") - } - assertThat(result).isEqualTo(file1.virtualFile) - } - } -} - -class JavaCodeWhispererFileCrawlerTest { - @Rule - @JvmField - val projectRule = JavaCodeInsightTestFixtureRule() - - @Rule - @JvmField - val disposableRule = DisposableRule() - - lateinit var sut: CodeWhispererFileCrawler - - lateinit var project: Project - lateinit var fixture: CodeInsightTestFixture - - @Before - fun setup() { - sut = JavaCodeWhispererFileCrawler - - project = projectRule.project - fixture = projectRule.fixture - } - - @Test - fun getFileDistance() { - val targetFile = fixture.addFileToProject("service/microService/CodeWhispererFileContextProvider.java", aString()) - - val fileWithDistance0 = fixture.addFileToProject("service/microService/CodeWhispererFileCrawler.java", aString()) - val fileWithDistance1 = fixture.addFileToProject("service/CodewhispererRecommendationService.java", aString()) - val fileWithDistance3 = fixture.addFileToProject("util/CodeWhispererConstants.java", aString()) - val fileWithDistance4 = fixture.addFileToProject("ui/popup/CodeWhispererPopupManager.java", aString()) - val fileWithDistance5 = fixture.addFileToProject("ui/popup/components/CodeWhispererPopup.java", aString()) - val fileWithDistance6 = fixture.addFileToProject("ui/popup/components/actions/AcceptRecommendationAction.java", aString()) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance0.virtualFile)) - .isEqualTo(0) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance1.virtualFile)) - .isEqualTo(1) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance3.virtualFile)) - .isEqualTo(3) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance4.virtualFile)) - .isEqualTo(4) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance5.virtualFile)) - .isEqualTo(5) - - assertThat(CodeWhispererFileCrawler.getFileDistance(targetFile.virtualFile, fileWithDistance6.virtualFile)) - .isEqualTo(6) - } - - @Test - fun `isTest - should return false`() { - val file1 = fixture.addFileToProject("src/utils/Foo.java", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isFalse - - val file2 = fixture.addFileToProject("src/controler/Bar.java", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isFalse - - val file3 = fixture.addFileToProject("Main.java", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isFalse - - val file4 = fixture.addFileToProject("component/dto/Boo.java", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isFalse - } - - @Test - fun `isTest - should return true`() { - val file1 = fixture.addFileToProject("tst/components/Foo.java", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isTrue - - val file2 = fixture.addFileToProject("test/components/Foo.java", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isTrue - - val file3 = fixture.addFileToProject("tests/components/Foo.java", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isTrue - - val file4 = fixture.addFileToProject("FooTest.java", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isTrue - - val file5 = fixture.addFileToProject("src/tst/services/FooServiceTest.java", "") - assertThat(sut.isTestFile(file5.virtualFile, project)).isTrue - - val file6 = fixture.addFileToProject("test/services/BarServiceTest.java", "") - assertThat(sut.isTestFile(file6.virtualFile, project)).isTrue - - val file7 = fixture.addFileToProject("FooTests.java", "") - assertThat(sut.isTestFile(file7.virtualFile, project)).isTrue - } - - @Test - fun listCrossFileCandidate() { - val recommendationServiceFile = fixture.addFileToProject("service/CodewhispererRecommendationService.java", aString()) - val fileContextProviderFile = fixture.addFileToProject("service/microService/CodeWhispererFileContextProvider.java", aString()) - val constantFile = fixture.addFileToProject("util/CodeWhispererConstants.java", aString()) - val popupManagerFile = fixture.addFileToProject("ui/popup/CodeWhispererPopupManager.java", aString()) - val popupFile = fixture.addFileToProject("ui/popup/components/CodeWhispererPopup.java", aString()) - val popupActionFile = fixture.addFileToProject("ui/popup/components/actions/AcceptRecommendationAction.java", aString()) - - val files = listOf(recommendationServiceFile, fileContextProviderFile, constantFile, popupManagerFile, popupFile, popupActionFile) - - files.shuffled().forEach { - runInEdtAndWait { - fixture.openFileInEditor(it.virtualFile) - } - } - - val actual = sut.listCrossFileCandidate(fileContextProviderFile) - assertThat(actual).isEqualTo( - listOf( - recommendationServiceFile.virtualFile, - constantFile.virtualFile, - popupManagerFile.virtualFile, - popupFile.virtualFile, - popupActionFile.virtualFile - ) - ) - } - - @Test - fun findFilesUnderProjectRoot() { - val mainClass = fixture.addFileToProject("Main.java", "") - val controllerClass = fixture.addFileToProject("service/controllers/MyController.java", "") - val anotherClass = fixture.addFileToProject("/utils/AnotherClass.java", "") - val notImportedClass = fixture.addFileToProject("/utils/NotImported.java", "") - val notImportedClass2 = fixture.addFileToProject("/utils/NotImported2.java", "") - - runReadAction { - val actual = sut.listFilesUnderProjectRoot(project) - val expected = listOf(mainClass, controllerClass, anotherClass, notImportedClass, notImportedClass2) - .map { it.virtualFile } - .toSet() - - assertThat(actual).hasSize(expected.size) - actual.forEach { - assertThat(expected.contains(it)).isTrue - } - } - } - - @Test - fun `listUtgCandidate by name`() { - val mainPsi = fixture.addFileToProject("Main.java", aString()) - fixture.addFileToProject("Class1.java", aString()) - fixture.addFileToProject("Class2.java", aString()) - fixture.addFileToProject("Class3.java", aString()) - val tstPsi = fixture.addFileToProject("/tst/java/MainTest.java", aString()) - - fun assertCrawlerFindCorrectFiles(sut: FileCrawler) { - runInEdtAndWait { - fixture.openFileInEditor(tstPsi.virtualFile) - - val actual = sut.listUtgCandidate(tstPsi) - - assertThat(actual.vfile).isNotNull.isEqualTo(mainPsi.virtualFile) - assertThat(actual.strategy).isNotNull.isEqualTo(UtgStrategy.ByName) - } - } - - assertCrawlerFindCorrectFiles(JavaCodeWhispererFileCrawler) - } - - @Test - fun `listUtgCandidate by content - java`() { - ExtensionTestUtil.maskExtensions(CodeWhispererClassResolver.EP_NAME, listOf(CodeWhispereJavaClassResolver()), disposableRule.disposable) - - val mainPsi = fixture.addFileToProject( - "Main.java", - """ - public class Main { - public static void main () { - runApp() - } - - public void runApp() { - // TODO - } - } - """.trimIndent() - ) - val file1 = fixture.addFileToProject("Class1.java", "trivial string 1") - val file2 = fixture.addFileToProject("Class2.java", "trivial string 2") - val file3 = fixture.addFileToProject("Class3.java", "trivial string 3") - val tstPsi = fixture.addFileToProject( - "/tst/java/MainTestNotFollowingNamingConvention.java", - """ - public class MainTest { - public void testRunApp() { - sut.runApp() - } - } - """.trimIndent() - ) - - runInEdtAndWait { - fixture.openFileInEditor(mainPsi.virtualFile) - fixture.openFileInEditor(file1.virtualFile) - fixture.openFileInEditor(file2.virtualFile) - fixture.openFileInEditor(file3.virtualFile) - fixture.openFileInEditor(tstPsi.virtualFile) - } - - runInEdtAndWait { - val openedFiles = EditorFactory.getInstance().allEditors.size - - val actual = sut.listUtgCandidate(tstPsi) - - assertThat(openedFiles).isEqualTo(5) - assertThat(actual.vfile).isNotNull.isEqualTo(mainPsi.virtualFile) - assertThat(actual.strategy).isNotNull.isEqualTo(UtgStrategy.ByContent) - } - } - - @Test - fun `test util countSubstringMatches`() { - val elementsToCheck = listOf("apple", "pineapple", "banana", "chocolate", "fries", "laptop", "amazon", "codewhisperer", "aws") - val targetElements = listOf( - "an apple a day, keep doctors away", - "codewhisperer is the best AI code generator", - "chocolateCake", - "green apple is sour", - "pineapple juice", - "chocolate cake is good" - ) - - val actual = CodeWhispererFileCrawler.countSubstringMatches(targetElements, elementsToCheck) - assertThat(actual).isEqualTo(4) - } - - @Test - fun `guessSourceFileName java`() { - val sut = JavaCodeWhispererFileCrawler - - assertThat(sut.guessSourceFileName("FooTest.java")).isEqualTo("Foo.java") - assertThat(sut.guessSourceFileName("FooBarTest.java")).isEqualTo("FooBar.java") - assertThat(sut.guessSourceFileName("Foo.java")).isNull() - assertThat(sut.guessSourceFileName("FooBar.java")).isNull() - } -} - -class PythonCodeWhispererFileCrawlerTest { - @JvmField - @Rule - val projectRule: CodeInsightTestFixtureRule = PythonCodeInsightTestFixtureRule() - - @JvmField - @Rule - val disposableRule = DisposableRule() - - lateinit var sut: CodeWhispererFileCrawler - - lateinit var project: Project - lateinit var fixture: CodeInsightTestFixture - - @Before - fun setup() { - sut = PythonCodeWhispererFileCrawler - - project = projectRule.project - fixture = projectRule.fixture - } - - @Test - fun `isTest - should return false`() { - val file1 = fixture.addFileToProject("src/utils/foo.py", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isFalse - - val file2 = fixture.addFileToProject("src/controler/bar.py", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isFalse - - val file3 = fixture.addFileToProject("main.py", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isFalse - - val file4 = fixture.addFileToProject("component/dto/boo.py", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isFalse - } - - @Test - fun `isTest - should return true`() { - val file1 = fixture.addFileToProject("tst/components/foo.py", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isTrue - - val file2 = fixture.addFileToProject("test/components/foo.py", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isTrue - - val file3 = fixture.addFileToProject("tests/components/foo.py", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isTrue - - val file4 = fixture.addFileToProject("foo_test.py", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isTrue - - val file5 = fixture.addFileToProject("test_foo.py", "") - assertThat(sut.isTestFile(file5.virtualFile, project)).isTrue - - val file6 = fixture.addFileToProject("src/tst/services/foo_service_test.py", "") - assertThat(sut.isTestFile(file6.virtualFile, project)).isTrue - - val file7 = fixture.addFileToProject("tests/services/test_bar_service.py", "") - assertThat(sut.isTestFile(file7.virtualFile, project)).isTrue - } - - @Test - fun `listUtgCandidate by name`() { - val mainPsi = fixture.addFileToProject("main.py", aString()) - fixture.addFileToProject("another_class.py", aString()) - fixture.addFileToProject("class2.py", aString()) - fixture.addFileToProject("class3.py", aString()) - val tstPsi = fixture.addFileToProject("/test/test_main.py", aString()) - - runInEdtAndWait { - fixture.openFileInEditor(tstPsi.virtualFile) - val actual = sut.listUtgCandidate(tstPsi) - assertThat(actual.vfile).isNotNull.isEqualTo(mainPsi.virtualFile) - assertThat(actual.strategy).isNotNull.isEqualTo(UtgStrategy.ByName) - } - } - - @Test - fun `listUtgCandidate by content - python`() { - ExtensionTestUtil.maskExtensions(CodeWhispererClassResolver.EP_NAME, listOf(CodeWhispererPythonClassResolver()), disposableRule.disposable) - - val mainPsi = fixture.addFileToProject( - "main.py", - """ - def add(num1, num2): - return num1 + num2 - - if __name__ == 'main': - - """.trimIndent() - ) - val file1 = fixture.addFileToProject("foo.py", "trivial string 1") - val file2 = fixture.addFileToProject("bar.py", "trivial string 2") - val file3 = fixture.addFileToProject("baz.py", "trivial string 3") - val tstPsi = fixture.addFileToProject( - "/test/main_test_not_following_naming_convention.py", - """ - class TestClass(unittest.TestCase): - def test_add_numbers(self): - result = add(1, 2) - self.assertEqual(result, 8, "") - """.trimIndent() - ) - - runInEdtAndWait { - fixture.openFileInEditor(mainPsi.virtualFile) - fixture.openFileInEditor(file1.virtualFile) - fixture.openFileInEditor(file2.virtualFile) - fixture.openFileInEditor(file3.virtualFile) - fixture.openFileInEditor(tstPsi.virtualFile) - } - - runInEdtAndWait { - val openedFiles = EditorFactory.getInstance().allEditors.size - - val actual = sut.listUtgCandidate(tstPsi) - - assertThat(openedFiles).isEqualTo(5) - assertThat(actual.vfile).isNotNull.isEqualTo(mainPsi.virtualFile) - assertThat(actual.strategy).isNotNull.isEqualTo(UtgStrategy.ByContent) - } - } - - @Test - fun `guessSourceFileName python`() { - val sut = PythonCodeWhispererFileCrawler - - assertThat(sut.guessSourceFileName("test_foo_bar.py")).isEqualTo("foo_bar.py") - assertThat(sut.guessSourceFileName("test_foo.py")).isEqualTo("foo.py") - assertThat(sut.guessSourceFileName("foo_test.py")).isEqualTo("foo.py") - assertThat(sut.guessSourceFileName("foo_test.py")).isEqualTo("foo.py") - assertThat(sut.guessSourceFileName("foo_bar_no_idea.py")).isNull() - } -} - -class JsCodeWhispererFileCrawlerTest { - @JvmField - @Rule - val projectRule: CodeInsightTestFixtureRule = CodeInsightTestFixtureRule() - - lateinit var fixture: CodeInsightTestFixture - lateinit var project: Project - - lateinit var sut: CodeWhispererFileCrawler - - @Before - fun setup() { - sut = JavascriptCodeWhispererFileCrawler - - project = projectRule.project - fixture = projectRule.fixture - } - - @Test - fun `isTest - should return false`() { - val file1 = fixture.addFileToProject("src/utils/foo.js", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isFalse - - val file2 = fixture.addFileToProject("src/controler/bar.jsx", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isFalse - - val file3 = fixture.addFileToProject("main.js", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isFalse - - val file4 = fixture.addFileToProject("component/dto/boo.jsx", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isFalse - } - - @Test - fun `isTest - should return true`() { - val file1 = fixture.addFileToProject("tst/components/foo.test.js", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isTrue - - val file2 = fixture.addFileToProject("test/components/foo.spec.js", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isTrue - - val file3 = fixture.addFileToProject("tests/components/foo.test.jsx", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isTrue - - val file4 = fixture.addFileToProject("foo.spec.jsx", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isTrue - - val file5 = fixture.addFileToProject("foo.test.js", "") - assertThat(sut.isTestFile(file5.virtualFile, project)).isTrue - - val file6 = fixture.addFileToProject("src/tst/services/fooService.test.js", "") - assertThat(sut.isTestFile(file6.virtualFile, project)).isTrue - - val file7 = fixture.addFileToProject("tests/services/barService.spec.jsx", "") - assertThat(sut.isTestFile(file7.virtualFile, project)).isTrue - - val file8 = fixture.addFileToProject("foo.Test.js", "") - assertThat(sut.isTestFile(file8.virtualFile, project)).isTrue - - val file9 = fixture.addFileToProject("foo.Spec.js", "") - assertThat(sut.isTestFile(file9.virtualFile, project)).isTrue - } - - @Test - fun `guessSourceFileName javascript`() { - assertThat(sut.guessSourceFileName("fooBar.test.js")).isEqualTo("fooBar.js") - assertThat(sut.guessSourceFileName("fooBar.spec.js")).isEqualTo("fooBar.js") - assertThat(sut.guessSourceFileName("fooBarNoIdea.js")).isNull() - } - - @Test - fun `guessSourceFileName jsx`() { - assertThat(sut.guessSourceFileName("fooBar.test.jsx")).isEqualTo("fooBar.jsx") - assertThat(sut.guessSourceFileName("fooBar.spec.jsx")).isEqualTo("fooBar.jsx") - assertThat(sut.guessSourceFileName("fooBarNoIdea.jsx")).isNull() - } -} - -class TsCodeWhispererFileCrawlerTest { - @JvmField - @Rule - val projectRule: CodeInsightTestFixtureRule = CodeInsightTestFixtureRule() - - lateinit var fixture: CodeInsightTestFixture - lateinit var project: Project - - lateinit var sut: CodeWhispererFileCrawler - - @Before - fun setup() { - sut = TypescriptCodeWhispererFileCrawler - - project = projectRule.project - fixture = projectRule.fixture - } - - @Test - fun `isTest - should return false`() { - val file1 = fixture.addFileToProject("src/utils/foo.ts", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isFalse - - val file2 = fixture.addFileToProject("src/controler/bar.tsx", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isFalse - - val file3 = fixture.addFileToProject("main.ts", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isFalse - - val file4 = fixture.addFileToProject("component/dto/boo.tsx", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isFalse - } - - @Test - fun `isTest - should return true`() { - val file1 = fixture.addFileToProject("tst/components/foo.test.ts", "") - assertThat(sut.isTestFile(file1.virtualFile, project)).isTrue - - val file2 = fixture.addFileToProject("test/components/foo.spec.ts", "") - assertThat(sut.isTestFile(file2.virtualFile, project)).isTrue - - val file3 = fixture.addFileToProject("tests/components/foo.test.tsx", "") - assertThat(sut.isTestFile(file3.virtualFile, project)).isTrue - - val file4 = fixture.addFileToProject("foo.spec.tsx", "") - assertThat(sut.isTestFile(file4.virtualFile, project)).isTrue - - val file5 = fixture.addFileToProject("foo.test.ts", "") - assertThat(sut.isTestFile(file5.virtualFile, project)).isTrue - - val file6 = fixture.addFileToProject("src/tst/services/fooService.test.ts", "") - assertThat(sut.isTestFile(file6.virtualFile, project)).isTrue - - val file7 = fixture.addFileToProject("tests/services/barService.spec.tsx", "") - assertThat(sut.isTestFile(file7.virtualFile, project)).isTrue - - val file8 = fixture.addFileToProject("foo.Test.ts", "") - assertThat(sut.isTestFile(file8.virtualFile, project)).isTrue - - val file9 = fixture.addFileToProject("foo.Spec.ts", "") - assertThat(sut.isTestFile(file9.virtualFile, project)).isTrue - } - - @Test - fun `guessSourceFileName typescript`() { - assertThat(sut.guessSourceFileName("fooBar.test.ts")).isEqualTo("fooBar.ts") - assertThat(sut.guessSourceFileName("fooBar.spec.ts")).isEqualTo("fooBar.ts") - assertThat(sut.guessSourceFileName("fooBarNoIdea.ts")).isNull() - } - - @Test - fun `guessSourceFileName tsx`() { - assertThat(sut.guessSourceFileName("fooBar.test.tsx")).isEqualTo("fooBar.tsx") - assertThat(sut.guessSourceFileName("fooBar.spec.tsx")).isEqualTo("fooBar.tsx") - assertThat(sut.guessSourceFileName("fooBarNoIdea.tsx")).isNull() - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererLanguageManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererLanguageManagerTest.kt index ad58bd62284..3d0f0728692 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererLanguageManagerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererLanguageManagerTest.kt @@ -247,84 +247,6 @@ class CodeWhispererProgrammingLanguageTest { override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Unknown } - @Test - fun `test language inline completion support`() { - suts.forEach { sut -> - val expected = when (sut) { - // supported - is CodeWhispererC, - is CodeWhispererCpp, - is CodeWhispererCsharp, - is CodeWhispererGo, - is CodeWhispererJava, - is CodeWhispererJavaScript, - is CodeWhispererJson, - is CodeWhispererJsx, - is CodeWhispererKotlin, - is CodeWhispererPhp, - is CodeWhispererPython, - is CodeWhispererRuby, - is CodeWhispererRust, - is CodeWhispererScala, - is CodeWhispererShell, - is CodeWhispererSql, - is CodeWhispererTf, - is CodeWhispererTsx, - is CodeWhispererTypeScript, - is CodeWhispererYaml, - is CodeWhispererDart, - is CodeWhispererLua, - is CodeWhispererPowershell, - is CodeWhispererR, - is CodeWhispererSwift, - is CodeWhispererSystemVerilog, - is CodeWhispererVue, - -> true - - // not supported - is CodeWhispererPlainText, is CodeWhispererUnknownLanguage -> false - - else -> false - } - - assertThat(sut.isCodeCompletionSupported()).isEqualTo(expected) - } - } - - @Test - fun `test language crossfile support`() { - suts.forEach { sut -> - val expected = when (sut) { - is CodeWhispererJava, - is CodeWhispererJavaScript, - is CodeWhispererJsx, - is CodeWhispererPython, - is CodeWhispererTsx, - is CodeWhispererTypeScript, - -> true - - else -> false - } - - assertThat(sut.isSupplementalContextSupported()).isEqualTo(expected) - } - } - - @Test - fun `test language utg support`() { - suts.forEach { sut -> - val expected = when (sut) { - is CodeWhispererJava, - is CodeWhispererPython, - -> true - - else -> false - } - - assertThat(sut.isUTGSupported()).isEqualTo(expected) - } - } - @Test fun `test CodeWhispererProgrammingLanguage isEqual will compare its languageId`() { val instance1: CodeWhispererJava = CodeWhispererJava.INSTANCE 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 d6900a556cb..de3c11d6616 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 @@ -14,6 +14,7 @@ import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customiz 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 @@ -487,7 +488,7 @@ class CodeWhispererModelConfiguratorTest { "" + "" - assertThat(actual).isEqualTo(expected) + assertThat(actual).isEqualToIgnoringWhitespace(expected) } @Test @@ -614,6 +615,7 @@ class CodeWhispererModelConfiguratorTest { assertThat(actual.previousAvailableCustomizations["fake-sso-url"]).isEqualTo(listOf("arn_1", "arn_2", "arn_3")) } + @Ignore @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)) @@ -622,11 +624,11 @@ class CodeWhispererModelConfiguratorTest { sut.switchCustomization(projectRule.project, oldCustomization) assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization) - - val fakeCustomizations = listOf( - CodeWhispererCustomization("oldArn", "oldName", "oldDescription") - ) - mockClintAdaptor.stub { on { listAvailableCustomizations(QRegionProfile("fake_name", "fake_arn")) } doReturn fakeCustomizations } + // TODO: mock sdk client to fix the test +// val fakeCustomizations = listOf( +// CodeWhispererCustomization("oldArn", "oldName", "oldDescription") +// ) +// mockClintAdaptor.stub { on { listAvailableCustomizations(QRegionProfile("fake_name", "fake_arn")) } doReturn fakeCustomizations } ApplicationManager.getApplication().messageBus .syncPublisher(QRegionProfileSelectedListener.TOPIC) @@ -635,6 +637,7 @@ class CodeWhispererModelConfiguratorTest { assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization) } + @Ignore @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)) @@ -642,10 +645,12 @@ class CodeWhispererModelConfiguratorTest { 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(QRegionProfile("fake_name", "fake_arn")) } doReturn fakeCustomizations } + + // TODO: mock sdk client to fix the test +// val fakeCustomizations = listOf( +// CodeWhispererCustomization("newArn", "newName", "newDescription") +// ) +// mockClintAdaptor.stub { on { listAvailableCustomizations(QRegionProfile("fake_name", "fake_arn")) } doReturn fakeCustomizations } val latch = CountDownLatch(1) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt index d1bce123572..a470ff97e6c 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt @@ -40,7 +40,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(0) - val expectedCount = pythonResponse.completions().size + val expectedCount = pythonResponse.items.size var expectedSelectedIndex: Int val navigationButton: JButton val oppositeButton: JButton diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt deleted file mode 100644 index c80416c986e..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2022 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.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.fixtures.CodeInsightTestFixture -import com.intellij.testFramework.replaceService -import com.intellij.testFramework.runInEdtAndWait -import org.assertj.core.api.Assertions.assertThat -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.spy -import org.mockito.kotlin.stub -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.aws.toolkits.core.utils.test.aString -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule - -class CodeWhispererRecommendationManagerTest { - @Rule - @JvmField - var projectRule = PythonCodeInsightTestFixtureRule() - - @Rule - @JvmField - val disposableRule = DisposableRule() - - private val documentContentContent = "012345678" - private lateinit var sut: CodeWhispererRecommendationManager - private lateinit var fixture: CodeInsightTestFixture - private lateinit var project: Project - - @Before - fun setup() { - fixture = projectRule.fixture - project = projectRule.project - - fixture.configureByText("test.py", documentContentContent) - runInEdtAndWait { - fixture.editor.caretModel.moveToOffset(documentContentContent.length) - } - sut = spy(CodeWhispererRecommendationManager.getInstance()) - ApplicationManager.getApplication().replaceService( - CodeWhispererRecommendationManager::class.java, - sut, - disposableRule.disposable - ) - } - - @Test - fun `test overlap()`() { - assertThat(sut.overlap("def", "abc")).isEqualTo("") - assertThat(sut.overlap("def", "fgh")).isEqualTo("f") - assertThat(sut.overlap(" ", " }")).isEqualTo(" ") - assertThat(sut.overlap("abcd", "abc")).isEqualTo("") - } - - @Test - fun `test recommendation will be discarded when it's a exact match to user's input`() { - val userInput = "def" - val detail = sut.buildDetailContext(aRequestContext(project), userInput, listOf(aCompletion("def")), aString()) - assertThat(detail[0].isDiscarded).isTrue - assertThat(detail[0].isTruncatedOnRight).isFalse - } - - @Test - fun `test duplicated recommendation after truncation will be discarded`() { - val userInput = "" - sut.stub { - onGeneric { findRightContextOverlap(any(), any()) } doReturn "}" - onGeneric { reformatReference(any(), any()) } doReturn aCompletion("def") - } - val detail = sut.buildDetailContext( - aRequestContext(project), - userInput, - listOf(aCompletion("def"), aCompletion("def}")), - aString() - ) - assertThat(detail[0].isDiscarded).isFalse - assertThat(detail[0].isTruncatedOnRight).isFalse - assertThat(detail[1].isDiscarded).isTrue - assertThat(detail[1].isTruncatedOnRight).isTrue - } - - @Test - fun `test blank recommendation after truncation will be discarded`() { - val userInput = "" - sut.stub { - onGeneric { findRightContextOverlap(any(), any()) } doReturn "}" - } - val detail = sut.buildDetailContext( - aRequestContext(project), - userInput, - listOf(aCompletion(" }")), - aString() - ) - assertThat(detail[0].isDiscarded).isTrue - assertThat(detail[0].isTruncatedOnRight).isTrue - } - - @Test - fun `overlap calculation should trim new line character starting from second character (index 1 of a string)`() { - // recommendation is wrapped inside |recommendationContent| - /** - * public foo() { - * re|turn foo - *}| - * public bar() { - * return bar - * } - */ - var overlap: String = sut.findRightContextOverlap(rightContext = " foo\n}\n\n\npublic bar () {\n\treturn bar\n}", recommendationContent = "turn foo\n}") - assertThat(overlap).isEqualTo(" foo\n}") - - /** - * public foo() { - * |return foo - * }| - * - * public bar() { - * return bar - * } - */ - overlap = sut.findRightContextOverlap(rightContext = "\n\n\n\npublic bar() {\n\treturn bar\n}", recommendationContent = "return foo\n}") - assertThat(overlap).isEqualTo("") - - /** - * println(|world)|; - * String foo = "foo"; - */ - overlap = sut.findRightContextOverlap(rightContext = "ld);\nString foo = \"foo\";", recommendationContent = "world)") - assertThat(overlap).isEqualTo("ld)") - - /** - * return |has_d_at_end| - * - * def foo: - * pass - */ - overlap = sut.findRightContextOverlap(rightContext = "\n\ndef foo():\n\tpass", recommendationContent = "has_d_at_end") - assertThat(overlap).isEqualTo("") - - /** - * { - * { foo: foo }, - * { bar: bar }, - * { |baz: baz }| - * } - * - */ - overlap = sut.findRightContextOverlap(rightContext = "\n}", recommendationContent = "baz: baz }") - assertThat(overlap).isEqualTo("") - - /** - * | - * - * foo| - * - */ - overlap = sut.findRightContextOverlap(rightContext = "\n\n\tfoo}", recommendationContent = "\n\tfoo") - assertThat(overlap).isEqualTo("\n\tfoo") - - /** A case we can't cover - * def foo(): - * |print(foo)| - * - * - * print(foo) - */ - overlap = sut.findRightContextOverlap(rightContext = "\n\n\n\tprint(foo)", recommendationContent = "print(foo)") - assertThat(overlap).isEqualTo("") - } - - @Test - fun `trim extra prefixing new line character`() { - var actual: String = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("") - assertThat(actual).isEqualTo("") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("f") - assertThat(actual).isEqualTo("f") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n") - assertThat(actual).isEqualTo("\n") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("foo") - assertThat(actual).isEqualTo("foo") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\nfoo") - assertThat(actual).isEqualTo("\nfoo") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n\nfoo\nbar") - assertThat(actual).isEqualTo("\nfoo\nbar") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n foo\nbar") - assertThat(actual).isEqualTo("\n foo\nbar") - - actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n\tfoo\nbar") - assertThat(actual).isEqualTo("\n\tfoo\nbar") - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferenceManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferenceManagerTest.kt index 6f952bc1bc1..8ce8947cce6 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferenceManagerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferenceManagerTest.kt @@ -11,9 +11,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.Reference -import software.amazon.awssdk.services.codewhispererruntime.model.Span import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule @@ -29,18 +26,6 @@ class CodeWhispererReferenceManagerTest { private val documentContentContent = "012345678\n9" private lateinit var fixture: CodeInsightTestFixture private lateinit var project: Project - private val originalReference = Reference.builder() - .licenseName("test_license") - .repository("test_repo") - .recommendationContentSpan( - Span.builder().start(0).end(14).build() - ) - .build() - - private val recommendation = Completion.builder() - .references(originalReference) - .content("test\nreference") - .build() @Before fun setup() { @@ -59,11 +44,4 @@ class CodeWhispererReferenceManagerTest { assertThat(referenceManager.getReferenceLineNums(fixture.editor, 0, 1)).isEqualTo("1") assertThat(referenceManager.getReferenceLineNums(fixture.editor, 0, 10)).isEqualTo("1 to 2") } - - @Test - fun `test getOriginalContent lines returns full reference lines`() { - val referenceManager = CodeWhispererCodeReferenceManager(project) - val expectedRecommendation = listOf("test", "reference") - assertThat(referenceManager.getOriginalContentLines(recommendation, 0)).isEqualTo(expectedRecommendation) - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt deleted file mode 100644 index f186740f1e8..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer - -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.stub -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.generateMockCompletionDetail -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.getReferenceInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.metadata -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.sdkHttpResponse - -class CodeWhispererReferencesTest : CodeWhispererTestBase() { - - @Test - fun `test references with invalid content span will remove the references - index out of bound`() { - testReferencesRangeValidity("test", 0, 5, invalid = true) - } - - @Test - fun `test references with invalid content span will remove the references - start greater than end`() { - testReferencesRangeValidity("test", 2, 1, invalid = true) - } - - @Test - fun `test references with valid content span will not remove the references`() { - testReferencesRangeValidity("test", 0, 4, invalid = false) - } - - @Test - fun `test references with valid(empty) content span will not remove the references`() { - testReferencesRangeValidity("test", 2, 2, invalid = false) - } - - private fun testReferencesRangeValidity(content: String, start: Int, end: Int, invalid: Boolean) { - val referenceInfo = getReferenceInfo() - val invalidReferencesResponse = GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail(content, referenceInfo.first, referenceInfo.second, start, end), - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - invalidReferencesResponse - } - } - - withCodeWhispererServiceInvokedAndWait { states -> - states.recommendationContext.details.forEach { - assertThat(it.recommendation.references().isEmpty()).isEqualTo(invalid) - } - popupManagerSpy.popupComponents.acceptButton.doClick() - } - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt deleted file mode 100644 index e4202cba6b3..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer - -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.stub -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext -import kotlin.random.Random - -class CodeWhispererRightContextTest : CodeWhispererTestBase() { - - @Test - fun `test recommendation equal to right context should not show recommendation`() { - val rightContext = pythonResponse.completions()[0].content() - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(true) - } - } - - @Test - fun `test right context resolution will remove reference span if reference is the same as right context`() { - val rightContext = pythonResponse.completions()[0].content() - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(true) - val details = states.recommendationContext.details - details.forEach { - assertThat(it.reformatted.references().isEmpty()) - } - } - } - - @Test - fun `test right context resolution will update range of reference if reference overlaps with right context`() { - val firstRecommendationContent = pythonResponse.completions()[0].content() - val lastNewLineIndex = firstRecommendationContent.lastIndexOf('\n') - val rightContext = firstRecommendationContent.substring(lastNewLineIndex) - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(false) - val firstDetail = states.recommendationContext.details[0] - val span = firstDetail.reformatted.references()[0].recommendationContentSpan() - assertThat(span.start()).isEqualTo(0) - assertThat(span.end()).isEqualTo(lastNewLineIndex) - } - } - - @Test - fun `test right context resolution will not change reference range if reference does not overlap with right context`() { - val firstRecommendationContent = pythonResponse.completions()[0].content() - val lastNewLineIndex = firstRecommendationContent.lastIndexOf('\n') - val rightContext = firstRecommendationContent.substring(lastNewLineIndex) - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - - val referenceInfo = CodeWhispererTestUtil.getReferenceInfo() - val referencesResponse = GenerateCompletionsResponse.builder() - .completions( - CodeWhispererTestUtil - .generateMockCompletionDetail(firstRecommendationContent, referenceInfo.first, referenceInfo.second, 0, lastNewLineIndex), - ) - .nextToken("") - .responseMetadata(CodeWhispererTestUtil.metadata) - .sdkHttpResponse(CodeWhispererTestUtil.sdkHttpResponse) - .build() as GenerateCompletionsResponse - - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - referencesResponse - } - } - - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(false) - val firstDetail = states.recommendationContext.details[0] - val span = firstDetail.reformatted.references()[0].recommendationContentSpan() - assertThat(span.start()).isEqualTo(0) - assertThat(span.end()).isEqualTo(lastNewLineIndex) - } - } - - @Test - fun `test suffix of recommendation equal to right context should truncate recommendation when remaining is single-line`() { - val firstRecommendation = pythonResponse.completions()[0].content() - val newLineIndex = firstRecommendation.indexOf('\n') - val remainingLength = Random.nextInt(1, newLineIndex) - val remaining = firstRecommendation.substring(0, remainingLength) - val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining) - popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) - } - } - - @Test - fun `test suffix of recommendation equal to right context should not truncate recommendation when remaining is multi-line`() { - val firstRecommendation = pythonResponse.completions()[0].content() - val newLineIndex = firstRecommendation.indexOf('\n') - val remainingLength = Random.nextInt(newLineIndex, firstRecommendation.length) - val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation) - } - } - - @Test - fun `test suffix of recommendation equal to prefix of right context should truncate recommendation when remaining is single-line`() { - val firstRecommendation = pythonResponse.completions()[0].content() - val newLineIndex = firstRecommendation.indexOf('\n') - val remainingLength = Random.nextInt(1, newLineIndex) - val remaining = firstRecommendation.substring(0, remainingLength) - val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) + "test" - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining) - popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) - } - } - - @Test - fun `test suffix of recommendation equal to prefix of right context should not truncate recommendation when remaining is multi-line`() { - val firstRecommendation = pythonResponse.completions()[0].content() - val newLineIndex = firstRecommendation.indexOf('\n') - val remainingLength = Random.nextInt(newLineIndex, firstRecommendation.length) - val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) + "test" - setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation) - } - } -} 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 46dd2551f15..37015577346 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 @@ -4,104 +4,64 @@ package software.aws.toolkits.jetbrains.services.codewhisperer import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.application.runReadAction import com.intellij.psi.PsiFile -import com.intellij.testFramework.DisposableRule import com.intellij.testFramework.replaceService import com.intellij.testFramework.runInEdtAndWait -import kotlinx.coroutines.async import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat 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.argumentCaptor import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.spy -import org.mockito.kotlin.times -import org.mockito.kotlin.verify +import org.mockito.kotlin.stub import org.mockito.kotlin.whenever -import software.amazon.awssdk.services.codewhispererruntime.model.FileContext -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage -import software.amazon.awssdk.services.codewhispererruntime.model.SupplementalContext -import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager -import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionTriggerKind +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil.toUriString import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition -import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider -import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule import software.aws.toolkits.telemetry.CodewhispererTriggerType -class CodeWhispererServiceTest { - @Rule - @JvmField - val projectRule = JavaCodeInsightTestFixtureRule() +class CodeWhispererServiceTest : CodeWhispererTestBase() { - @Rule - @JvmField - val disposableRule = DisposableRule() - - private lateinit var sut: CodeWhispererService private lateinit var customizationConfig: CodeWhispererModelConfigurator - private lateinit var clientFacade: CodeWhispererClientAdaptor - private lateinit var popupManager: CodeWhispererPopupManager - private lateinit var telemetryService: CodeWhispererTelemetryService - private lateinit var mockPopup: JBPopup private lateinit var file: PsiFile @Before - fun setUp() { - sut = CodeWhispererService.getInstance() + override fun setUp() { + super.setUp() customizationConfig = mock() - clientFacade = mock() - mockPopup = mock() - popupManager = mock { - on { initPopup() } doReturn mockPopup - } - - telemetryService = mock() - file = projectRule.fixture.addFileToProject("main.java", "public class Main {}") runInEdtAndWait { projectRule.fixture.openFileInEditor(file.virtualFile) } ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, customizationConfig, disposableRule.disposable) - ApplicationManager.getApplication().replaceService(CodeWhispererTelemetryService::class.java, telemetryService, disposableRule.disposable) - ApplicationManager.getApplication().replaceService(CodeWhispererPopupManager::class.java, popupManager, disposableRule.disposable) - - projectRule.project.replaceService(CodeWhispererClientAdaptor::class.java, clientFacade, disposableRule.disposable) - projectRule.project.replaceService(AwsConnectionManager::class.java, mock(), disposableRule.disposable) } @Test - fun `getRequestContext should use correct fileContext and timeout to fetch supplementalContext`() = runTest { + fun `getRequestContext should use correct fileContext`() = runTest { val fileContextProvider = FileContextProvider.getInstance(projectRule.project) val fileContextProviderSpy = spy(fileContextProvider) projectRule.project.replaceService(FileContextProvider::class.java, fileContextProviderSpy, disposableRule.disposable) - val requestContext = sut.getRequestContext( + codewhispererService.stub { + onGeneric { + getRequestContext(any(), any(), any(), any(), any()) + }.thenCallRealMethod() + } + + val requestContext = codewhispererService.getRequestContext( TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, CodeWhispererAutomatedTriggerType.Enter()), editor = projectRule.fixture.editor, project = projectRule.project, @@ -109,22 +69,19 @@ class CodeWhispererServiceTest { LatencyContext() ) - requestContext.awaitSupplementalContext() - val fileContextCaptor = argumentCaptor() - verify(fileContextProviderSpy, times(1)).extractSupplementalFileContext(eq(file), fileContextCaptor.capture(), eq(100)) - assertThat(fileContextCaptor.firstValue).isEqualTo( + assertThat(requestContext.fileContextInfo).isEqualTo( FileContextInfo( CaretContext(leftFileContext = "", rightFileContext = "public class Main {}", leftContextOnCurrentLine = ""), "main.java", CodeWhispererJava.INSTANCE, "main.java", - "temp:///src/main.java" + file.virtualFile.url ) ) } @Test - fun `getRequestContext should have supplementalContext and customizatioArn if they're present`() { + fun `getRequestContext should have customizationArn if it's present`() { whenever(customizationConfig.activeCustomization(projectRule.project)).thenReturn( CodeWhispererCustomization( "fake-arn", @@ -133,24 +90,18 @@ class CodeWhispererServiceTest { ) ) - val mockSupplementalContext = aSupplementalContextInfo( - myContents = listOf( - Chunk(content = "foo", path = "/foo.java"), - Chunk(content = "bar", path = "/bar.java"), - Chunk(content = "baz", path = "/baz.java") - ), - myIsUtg = false, - myLatency = 50L - ) - val mockFileContextProvider = mock { on { this.extractFileContext(any(), any()) } doReturn aFileContextInfo() - onBlocking { this.extractSupplementalFileContext(any(), any(), any()) } doReturn mockSupplementalContext } projectRule.project.replaceService(FileContextProvider::class.java, mockFileContextProvider, disposableRule.disposable) + codewhispererService.stub { + onGeneric { + getRequestContext(any(), any(), any(), any(), any()) + }.thenCallRealMethod() + } - val actual = sut.getRequestContext( + val actual = codewhispererService.getRequestContext( TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), projectRule.fixture.editor, projectRule.project, @@ -158,98 +109,24 @@ class CodeWhispererServiceTest { LatencyContext() ) - runTest { - actual.awaitSupplementalContext() - } - assertThat(actual.customizationArn).isEqualTo("fake-arn") - assertThat(actual.supplementalContext).isEqualTo(mockSupplementalContext) } - @Ignore("need update language type since Java is fully supported") @Test - fun `getRequestContext - cross file context should be empty for non-cross-file user group`() { - val file = projectRule.fixture.addFileToProject("main.java", "public class Main {}") - - runInEdtAndWait { - projectRule.fixture.openFileInEditor(file.virtualFile) - } + fun `test handleInlineCompletion creates correct params and sends to server`() = runTest { + val mockEditor = projectRule.fixture.editor - val actual = sut.getRequestContext( + val capturedParams = codewhispererService.createInlineCompletionParams( + mockEditor, TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), - projectRule.fixture.editor, - projectRule.project, - file, - LatencyContext() - ) - - assertThat(actual.supplementalContext).isNotNull - assertThat(actual.supplementalContext?.contents).isEmpty() - assertThat(actual.supplementalContext?.contentLength).isEqualTo(0) - } - - @Test - fun `given request context, should invoke service API with correct args and await supplemental context deferred`() = runTest { - val mockFileContext = aFileContextInfo(CodeWhispererJava.INSTANCE) - val mockSupContext = spy( - aSupplementalContextInfo( - myContents = listOf( - Chunk(content = "foo", path = "/foo.java"), - Chunk(content = "bar", path = "/bar.java"), - Chunk(content = "baz", path = "/baz.java") - ), - myIsUtg = false, - myLatency = 50L - ) - ) - - val mockRequestContext = spy( - RequestContext( - project = projectRule.project, - editor = projectRule.fixture.editor, - triggerTypeInfo = TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, CodeWhispererAutomatedTriggerType.Enter()), - caretPosition = CaretPosition(0, 0), - fileContextInfo = mockFileContext, - supplementalContextDeferred = async { mockSupContext }, - connection = ToolkitConnectionManager.getInstance(projectRule.project).activeConnection(), - latencyContext = LatencyContext(), - customizationArn = "fake-arn", - profileArn = "fake-arn", - workspaceId = null, - diagnostics = emptyList() - ) + null ) - sut.invokeCodeWhispererInBackground(mockRequestContext).join() - - verify(mockRequestContext, times(1)).awaitSupplementalContext() - verify(clientFacade).generateCompletionsPaginator(any()) - - argumentCaptor { - verify(clientFacade).generateCompletionsPaginator(capture()) - assertThat(firstValue.customizationArn()).isEqualTo("fake-arn") - assertThat(firstValue.fileContext()).isEqualTo(mockFileContext.toSdkModel()) - assertThat(firstValue.supplementalContexts()).hasSameSizeAs(mockSupContext.contents) - assertThat(firstValue.supplementalContexts()).isEqualTo(mockSupContext.toSdkModel()) + runReadAction { + assertThat(capturedParams.textDocument.uri).isEqualTo(toUriString(file.virtualFile)) + assertThat(capturedParams.position.line).isEqualTo(mockEditor.caretModel.primaryCaret.visualPosition.line) + assertThat(capturedParams.position.character).isEqualTo(mockEditor.caretModel.primaryCaret.offset) + assertThat(capturedParams.context.triggerKind).isEqualTo(InlineCompletionTriggerKind.Invoke) } } } - -private fun CodeWhispererProgrammingLanguage.toSdkModel(): ProgrammingLanguage = ProgrammingLanguage.builder() - .languageName(toCodeWhispererRuntimeLanguage().languageId) - .build() - -private fun FileContextInfo.toSdkModel(): FileContext = FileContext.builder() - .filename(fileRelativePath) - .fileUri(fileUri) - .programmingLanguage(programmingLanguage.toCodeWhispererRuntimeLanguage().toSdkModel()) - .leftFileContent(caretContext.leftFileContext) - .rightFileContent(caretContext.rightFileContext) - .build() - -private fun SupplementalContextInfo.toSdkModel(): List = contents.map { - SupplementalContext.builder() - .content(it.content) - .filePath(it.path) - .build() -} 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 84263c62ad2..1adcd2a795e 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 @@ -4,7 +4,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer import com.intellij.analysis.problemsView.toolWindow.ProblemsView -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.wm.RegisterToolWindowTask import com.intellij.openapi.wm.ToolWindow @@ -13,9 +12,7 @@ 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 @@ -24,15 +21,12 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any 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.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 -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.jetbrains.services.codewhisperer.status.CodeWhispererStatusBarWidgetFactory import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceToolWindowFactory import software.aws.toolkits.jetbrains.settings.CodeWhispererConfiguration @@ -42,7 +36,6 @@ import kotlin.test.fail class CodeWhispererSettingsTest : CodeWhispererTestBase() { - private lateinit var codewhispererServiceSpy: CodeWhispererService private lateinit var toolWindowHeadlessManager: ToolWindowHeadlessManagerImpl @get:Rule @@ -51,9 +44,6 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { @Before override fun setUp() { super.setUp() - codewhispererServiceSpy = spy(codewhispererService) - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - // Create a mock ToolWindowManager with working implementation of setAvailable() and isAvailable() toolWindowHeadlessManager = object : ToolWindowHeadlessManagerImpl(projectRule.project) { private val myToolWindows: MutableMap = HashMap() @@ -94,7 +84,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { whenever(stateManager.checkActiveCodeWhispererConnectionType(projectRule.project)).thenReturn(CodeWhispererLoginType.Logout) assertThat(isCodeWhispererEnabled(projectRule.project)).isFalse invokeCodeWhispererService() - verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any()) + verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any()) } @Test @@ -103,7 +93,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { assertThat(stateManager.isAutoEnabled()).isFalse runInEdtAndWait { projectRule.fixture.type(':') - verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any()) + verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any()) } } @@ -254,18 +244,6 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { 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/CodeWhispererStateTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt index 0397576e8b6..3588cc4f707 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt @@ -9,7 +9,6 @@ import org.junit.Test import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.telemetry.CodewhispererLanguage import software.aws.toolkits.telemetry.CodewhispererTriggerType @@ -44,16 +43,6 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { } } - @Test - fun `test CodeWhisperer invocation sets response metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> - val actualResponseContext = states.responseContext - assertThat(listOf(actualResponseContext.sessionId)).isEqualTo( - pythonResponse.sdkHttpResponse().headers()[CodeWhispererService.KET_SESSION_ID] - ) - } - } - @Test fun `test CodeWhisperer invocation sets recommendation metadata correctly`() { withCodeWhispererServiceInvokedAndWait { states -> @@ -61,12 +50,12 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { val (actualDetailContexts, actualUserInput) = actualRecommendationContext assertThat(actualUserInput).isEqualTo("") - val expectedCount = pythonResponse.completions().size + val expectedCount = pythonResponse.items.size assertThat(actualDetailContexts.size).isEqualTo(expectedCount) actualDetailContexts.forEachIndexed { i, actualDetailContext -> - val (actualRequestId, actualRecommendationDetail, _, actualIsDiscarded) = actualDetailContext - assertThat(actualRecommendationDetail.content()).isEqualTo(pythonResponse.completions()[i].content()) - assertThat(actualRequestId).isEqualTo(pythonResponse.responseMetadata().requestId()) + val (actualItemId, actualCompletion, actualIsDiscarded) = actualDetailContext + assertThat(actualCompletion.insertText).isEqualTo(pythonResponse.items[i].insertText) + assertThat(actualItemId).isEqualTo(pythonResponse.items[i].itemId) assertThat(actualIsDiscarded).isEqualTo(false) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt deleted file mode 100644 index 63140875922..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright 2023 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.application.ApplicationManager -import com.intellij.testFramework.ApplicationRule -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.ProjectRule -import com.intellij.testFramework.replaceService -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doNothing -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.stub -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever -import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse -import software.aws.toolkits.core.telemetry.MetricEvent -import software.aws.toolkits.core.telemetry.TelemetryBatcher -import software.aws.toolkits.core.telemetry.TelemetryPublisher -import software.aws.toolkits.jetbrains.core.credentials.LegacyManagedBearerSsoConnection -import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager -import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection -import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor -import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher -import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService -import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState -import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import java.time.Duration -import kotlin.random.Random - -// TODO: add more tests -class CodeWhispererTelemetryServiceTest { - private class NoOpToolkitTelemetryService( - publisher: TelemetryPublisher = NoOpPublisher(), - batcher: TelemetryBatcher, - ) : TelemetryService(publisher, batcher) - - @Rule - @JvmField - val applicationRule = ApplicationRule() - - @Rule - @JvmField - val projectRule = ProjectRule() - - @Rule - @JvmField - val disposableRule = DisposableRule() - - private lateinit var sut: CodeWhispererTelemetryService - private lateinit var telemetryServiceSpy: TelemetryService - private lateinit var batcher: TelemetryBatcher - private lateinit var mockClient: CodeWhispererClientAdaptor - - @Before - fun setup() { - AwsSettings.getInstance().isTelemetryEnabled = true - - sut = spy(CodeWhispererTelemetryService.getInstance()) - - batcher = mock() - - telemetryServiceSpy = NoOpToolkitTelemetryService(batcher = batcher) - ApplicationManager.getApplication().replaceService(TelemetryService::class.java, telemetryServiceSpy, disposableRule.disposable) - - mockClient = spy(CodeWhispererClientAdaptor.getInstance(projectRule.project)) - mockClient.stub { - onGeneric { - sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any(), any()) - }.doAnswer { - mock() - } - } - projectRule.project.replaceService(CodeWhispererClientAdaptor::class.java, mockClient, disposableRule.disposable) - } - - @After - fun cleanup() { - sut.previousDecisions().clear() - } - - @Test - fun `test recordSuggestionState`() { - fun assertSuggestionStates(expectedStates: List) { - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(expectedStates) - val hasUserAccepted = expectedStates.any { it == CodewhispererSuggestionState.Accept } - - val details = recommendationContext.details - val actualStates = mutableListOf() - details.forEachIndexed { index, detail -> - val suggestionState = sut.recordSuggestionState( - index, - sessionContext.selectedIndex, - sessionContext.seen.contains(index), - hasUserAccepted, - detail.isDiscarded, - detail.recommendation.content().isEmpty() - ) - actualStates.add(suggestionState) - } - - assertThat(actualStates).hasSize(expectedStates.size) - actualStates.forEachIndexed { i, actual -> - assertThat(actual).isEqualTo(expectedStates[i]) - } - } - - assertSuggestionStates(listOf(CodewhispererSuggestionState.Ignore, CodewhispererSuggestionState.Ignore, CodewhispererSuggestionState.Accept)) - assertSuggestionStates( - listOf( - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Unseen - ) - ) - assertSuggestionStates(listOf(CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Empty)) - assertSuggestionStates( - listOf( - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Discard - ) - ) - assertSuggestionStates( - listOf( - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Accept, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Unseen - ) - ) - } - - @Test - fun `test aggregateUserDecision`() { - fun assertAggregateUserDecision(decisions: List, expected: CodewhispererPreviousSuggestionState) { - val actual = sut.aggregateUserDecision(decisions) - assertThat(actual).isEqualTo(expected) - } - - assertAggregateUserDecision( - listOf( - CodewhispererSuggestionState.Accept, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Unseen - ), - CodewhispererPreviousSuggestionState.Accept - ) - - assertAggregateUserDecision( - listOf( - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Ignore - ), - CodewhispererPreviousSuggestionState.Reject - ) - - assertAggregateUserDecision( - listOf( - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Discard, - CodewhispererSuggestionState.Empty - ), - CodewhispererPreviousSuggestionState.Discard - ) - - assertAggregateUserDecision( - listOf( - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty - ), - CodewhispererPreviousSuggestionState.Empty - ) - } - - @Test - fun `sendUserTriggerDecisionEvent`() { - val timeSinceDocumentChanged = Random.nextDouble(0.0, 1000.0) - val mockCodeWhispererInvocationStatus: CodeWhispererInvocationStatus = mock { - on { getTimeSinceDocumentChanged() }.thenReturn(timeSinceDocumentChanged) - } - - ApplicationManager.getApplication().replaceService( - CodeWhispererInvocationStatus::class.java, - mockCodeWhispererInvocationStatus, - disposableRule.disposable - ) - - val supplementalContextInfo = aSupplementalContextInfo() - val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also { - runTest { it.awaitSupplementalContext() } - } - val responseContext = aResponseContext() - val recommendationContext = aRecommendationContext() - val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - val suggestionState = CodewhispererSuggestionState.Reject - val suggestionReferenceCount = Random.nextInt(2) - val lineCount = Random.nextInt(0, 100) - val charCount = Random.nextInt(0, 100) - - val expectedTotalImportCount = recommendationContext.details.fold(0) { grandTotal, detail -> - grandTotal + detail.recommendation.mostRelevantMissingImports().size - } - - sut.sendUserTriggerDecisionEvent( - requestContext, - responseContext, - recommendationContext, - suggestionState, - popupShownDuration, - suggestionReferenceCount, - lineCount, - charCount - ) - - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount( - allValues, - "codewhisperer_userTriggerDecision", - 1, - "codewhispererSessionId" to responseContext.sessionId, - "codewhispererFirstRequestId" to requestContext.latencyContext.firstRequestId, - "codewhispererCompletionType" to recommendationContext.details[0].completionType, - "codewhispererLanguage" to requestContext.fileContextInfo.programmingLanguage.toTelemetryType(), - "codewhispererTriggerType" to requestContext.triggerTypeInfo.triggerType, - "codewhispererAutomatedTriggerType" to requestContext.triggerTypeInfo.automatedTriggerType.telemetryType, - "codewhispererLineNumber" to requestContext.caretPosition.line, - "codewhispererCursorOffset" to requestContext.caretPosition.offset, - "codewhispererSuggestionCount" to recommendationContext.details.size, - "codewhispererSuggestionImportCount" to expectedTotalImportCount, - "codewhispererTotalShownTime" to popupShownDuration?.toMillis()?.toDouble(), - "codewhispererTriggerCharacter" to requestContext.triggerTypeInfo.automatedTriggerType.let { - if (it is CodeWhispererAutomatedTriggerType.SpecialChar) { - it.specialChar.toString() - } else { - null - } - }, - "codewhispererTypeaheadLength" to recommendationContext.userInputSinceInvocation.length, - "codewhispererTimeSinceLastDocumentChange" to timeSinceDocumentChanged, - "codewhispererSupplementalContextTimeout" to supplementalContextInfo.isProcessTimeout, - "codewhispererSupplementalContextIsUtg" to supplementalContextInfo.isUtg, - "codewhispererSupplementalContextLength" to supplementalContextInfo.contentLength, - "codewhispererCharactersAccepted" to charCount - ) - } - } - - @Test - fun `sendUserDecisionEventForAll will send userDecision event for all suggestions`() { - doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any()) - val eventCount = mutableMapOf() - var totalEventCount = 0 - val requestContext = aRequestContext(projectRule.project) - val responseContext = aResponseContext() - - fun assertUserDecision(decisions: List) { - decisions.forEach { eventCount[it] = 1 + (eventCount[it] ?: 0) } - totalEventCount += decisions.size - - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(decisions) - val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept } - val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - - sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration) - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - - eventCount.forEach { (k, v) -> - CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount( - allValues, - "codewhisperer_userDecision", - count = v, - "codewhispererSuggestionState" to k - ) - } - } - } - - assertUserDecision( - listOf( - CodewhispererSuggestionState.Accept, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Unseen - ) - ) - - assertUserDecision( - listOf( - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Unseen - ), - ) - - assertUserDecision( - listOf( - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty - ), - ) - } - - @Test - fun `sendUserDecisionEventForAll will aggregate suggestion level decisions and send out userTriggerDecision`() { - fun helper( - decisions: List, - expectedState: CodewhispererSuggestionState, - expectedPreviousSuggestionState: CodewhispererPreviousSuggestionState?, - ) { - val supplementalContextInfo = aSupplementalContextInfo() - val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also { - runTest { it.awaitSupplementalContext() } - } - val responseContext = aResponseContext() - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(decisions) - val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept } - val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - - sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration) - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - - CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount( - allValues, - "codewhisperer_userTriggerDecision", - count = 1, - "codewhispererSuggestionState" to expectedState, - "codewhispererSupplementalContextTimeout" to supplementalContextInfo.isProcessTimeout, - "codewhispererSupplementalContextIsUtg" to supplementalContextInfo.isUtg, - "codewhispererSupplementalContextLength" to supplementalContextInfo.contentLength, - atLeast = true - ) - - CodeWhispererTelemetryTest.assertEventsContainsFieldsAndCount( - allValues, - "codewhisperer_userTriggerDecision", - count = 1, - "codewhispererPreviousSuggestionState" to expectedPreviousSuggestionState, - "codewhispererSupplementalContextTimeout" to supplementalContextInfo.isProcessTimeout, - "codewhispererSupplementalContextIsUtg" to supplementalContextInfo.isUtg, - "codewhispererSupplementalContextLength" to supplementalContextInfo.contentLength, - atLeast = true - ) - } - } - - val decisionQueue = sut.previousDecisions() - assertThat(decisionQueue).isEmpty() - - helper( - listOf( - CodewhispererSuggestionState.Accept, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Ignore, - CodewhispererSuggestionState.Unseen - ), - CodewhispererSuggestionState.Accept, - null - ) - assertThat(decisionQueue).hasSize(1) - - helper( - listOf( - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Reject, - CodewhispererSuggestionState.Unseen, - CodewhispererSuggestionState.Unseen - ), - CodewhispererSuggestionState.Reject, - CodewhispererPreviousSuggestionState.Accept - ) - assertThat(decisionQueue).hasSize(2) - - helper( - listOf( - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty, - CodewhispererSuggestionState.Empty - ), - CodewhispererSuggestionState.Empty, - CodewhispererPreviousSuggestionState.Reject - ) - assertThat(decisionQueue).hasSize(3) - } - - private fun testSendTelemetryEventWithDifferentConfigsHelper(isProTier: Boolean, isTelemetryEnabled: Boolean) { - val mockStatesManager = mock() - ApplicationManager.getApplication().replaceService( - CodeWhispererExplorerActionManager::class.java, - mockStatesManager, - disposableRule.disposable - ) - - val mockSsoConnection = mock { - on { this.startUrl } doReturn if (isProTier) "fake sso url" else SONO_URL - } - - projectRule.project.replaceService( - ToolkitConnectionManager::class.java, - mock { on { activeConnectionForFeature(eq(CodeWhispererConnection.getInstance())) } doReturn mockSsoConnection }, - disposableRule.disposable - ) - AwsSettings.getInstance().isTelemetryEnabled = isTelemetryEnabled - - val expectedRequestContext = aRequestContext(projectRule.project) - val expectedResponseContext = aResponseContext() - val expectedRecommendationContext = aRecommendationContext() - val expectedSuggestionState = aSuggestionState() - val expectedDuration = Duration.ofSeconds(1) - val expectedSuggestionReferenceCount = 1 - val expectedGeneratedLineCount = 50 - val expectedCharCount = 100 - val expectedCompletionType = expectedRecommendationContext.details[0].completionType - sut.sendUserTriggerDecisionEvent( - expectedRequestContext, - expectedResponseContext, - expectedRecommendationContext, - expectedSuggestionState, - expectedDuration, - expectedSuggestionReferenceCount, - expectedGeneratedLineCount, - expectedCharCount - ) - - if (isProTier || isTelemetryEnabled) { - verify(mockClient).sendUserTriggerDecisionTelemetry( - eq(expectedRequestContext), - eq(expectedResponseContext), - eq(expectedCompletionType), - eq(expectedSuggestionState), - eq(expectedSuggestionReferenceCount), - eq(expectedGeneratedLineCount), - eq(expectedRecommendationContext.details.size), - eq(expectedCharCount) - ) - } else { - verifyNoInteractions(mockClient) - } - } - - @Test - fun `should invoke sendTelemetryEvent if opt out telemetry, for SSO users`() { - testSendTelemetryEventWithDifferentConfigsHelper(isProTier = true, isTelemetryEnabled = false) - } - - @Test - fun `should not invoke sendTelemetryEvent if opt out telemetry, for Builder ID users`() { - testSendTelemetryEventWithDifferentConfigsHelper(isProTier = false, isTelemetryEnabled = false) - } - - @Test - fun `should invoke sendTelemetryEvent if opt in telemetry, for SSO users`() { - testSendTelemetryEventWithDifferentConfigsHelper(isProTier = true, isTelemetryEnabled = true) - } - - @Test - fun `should invoke sendTelemetryEvent if opt in telemetry, for Builder ID users`() { - testSendTelemetryEventWithDifferentConfigsHelper(isProTier = false, isTelemetryEnabled = true) - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt index 954fc6a8dd8..430f813842a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt @@ -3,84 +3,29 @@ package software.aws.toolkits.jetbrains.services.codewhisperer -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.psi.PsiDocumentManager import com.intellij.testFramework.TestActionEvent import com.intellij.testFramework.replaceService -import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test -import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.atLeast -import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doNothing -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doReturnConsecutively import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.spy -import org.mockito.kotlin.stub -import org.mockito.kotlin.timeout -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import software.amazon.awssdk.awscore.DefaultAwsResponseMetadata -import software.amazon.awssdk.awscore.util.AwsHeader -import software.amazon.awssdk.http.SdkHttpResponse -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse import software.aws.toolkits.core.telemetry.MetricEvent import software.aws.toolkits.core.telemetry.TelemetryBatcher import software.aws.toolkits.core.telemetry.TelemetryPublisher -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.emptyListResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.keystrokeInput -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.listOfEmptyRecommendationResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.listOfMixedEmptyAndNonEmptyRecommendationResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponseWithNonEmptyToken -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testCodeWhispererException -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testRequestId -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testRequestIdForCodeWhispererException -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testSessionId -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType -import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager -import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Pause import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Resume -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.AcceptedSuggestionEntry -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererRuntime -import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import software.aws.toolkits.telemetry.CodewhispererTriggerType -import software.aws.toolkits.telemetry.Result -import java.time.Instant class CodeWhispererTelemetryTest : CodeWhispererTestBase() { - private val userDecision = "codewhisperer_userDecision" - private val userModification = "codewhisperer_userModification" - private val serviceInvocation = "codewhisperer_serviceInvocation" - private val codePercentage = "codewhisperer_codePercentage" private val awsModifySetting = "aws_modifySetting" - private val codewhispererSuggestionState = "codewhispererSuggestionState" private class TestTelemetryService( publisher: TelemetryPublisher = NoOpPublisher(), @@ -104,588 +49,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { } @Test - fun `test pre-setup failure will send service invocation event with failed status`() { - val codewhispererServiceSpy = spy(codewhispererService) { - onGeneric { getRequestContext(any(), any(), any(), any(), any()) } - .doAnswer { throw Exception() } - } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - - invokeCodeWhispererService() - - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "result" to Result.Failed.toString() - ) - } - } - - @Test - fun `test accepting recommendation will send user modification events with 1 accepted other unseen`() { - val trackerSpy = spy(CodeWhispererUserModificationTracker.getInstance(projectRule.project)) - projectRule.project.replaceService(CodeWhispererUserModificationTracker::class.java, trackerSpy, disposableRule.disposable) - - runInEdtAndWait { - val fakeSuggestionEntry = AcceptedSuggestionEntry( - Instant.now().minusSeconds(350), - projectRule.fixture.file.virtualFile, - projectRule.fixture.editor.document.createRangeMarker(0, 0, true), - "", - testSessionId, - testRequestId, - 0, - CodewhispererTriggerType.OnDemand, - CodewhispererCompletionType.Line, - CodeWhispererJava.INSTANCE, - CodewhispererRuntime.Java11, - "", - null - ) - trackerSpy.stub { - onGeneric { - trackerSpy.enqueue(any()) - }.doAnswer { - trackerSpy.enqueue(fakeSuggestionEntry) - }.thenCallRealMethod() - } - } - - withCodeWhispererServiceInvokedAndWait { - popupManagerSpy.popupComponents.acceptButton.doClick() - } - - trackerSpy.dispose() - val count = pythonResponse.completions().size - argumentCaptor().apply { - // 1 serviceInvocation + 1 userModification + 1 userDecision for accepted + - // (count - 1) userDecisions for ignored - verify(batcher, atLeast(2 + count)).enqueue(capture()) - assertEventsContainsFieldsAndCount(allValues, serviceInvocation, 1) - assertEventsContainsFieldsAndCount( - allValues, - userModification, - 1, - "codewhispererSessionId" to testSessionId, - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - 1, - codewhispererSuggestionState to CodewhispererSuggestionState.Accept.toString(), - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - count - 1, - codewhispererSuggestionState to CodewhispererSuggestionState.Unseen.toString(), - ) - } - } - - @Test - fun `test cancelling popup will send user decision event for all unseen but one rejected`() { - withCodeWhispererServiceInvokedAndWait { states -> - popupManagerSpy.cancelPopup(states.popup) - - val count = pythonResponse.completions().size - argumentCaptor().apply { - verify(batcher, atLeast(1 + count)).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "result" to Result.Succeeded.toString(), - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - 1, - codewhispererSuggestionState to CodewhispererSuggestionState.Reject.toString(), - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - count - 1, - codewhispererSuggestionState to CodewhispererSuggestionState.Unseen.toString(), - ) - } - } - } - - @Test - fun `test invoking CodeWhisperer will send service invocation event with succeeded status`() { - withCodeWhispererServiceInvokedAndWait { - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "result" to Result.Succeeded.toString(), - ) - } - } - } - - @Test - fun `test moving caret backwards before getting back response will send user decision events for all discarded`() { - val editorManagerSpy = spy(editorManager) - editorManagerSpy.stub { - onGeneric { - getCaretMovement(any(), any()) - } doAnswer { - CaretMovement.MOVE_BACKWARD - } - } - ApplicationManager.getApplication().replaceService(CodeWhispererEditorManager::class.java, editorManagerSpy, disposableRule.disposable) - - invokeCodeWhispererService() - - val count = pythonResponse.completions().size - runInEdtAndWait { - argumentCaptor().apply { - verify(batcher, atLeast(1 + count)).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "result" to Result.Succeeded.toString(), - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - count, - codewhispererSuggestionState to CodewhispererSuggestionState.Discard.toString(), - ) - } - } - } - - @Test - fun `test user's typeahead before getting response will discard recommendations whose prefix not matching`() { - val userInput = "(x, y)" - addUserInputAfterInvocation(userInput) - withCodeWhispererServiceInvokedAndWait { } - val prefixNotMatchCount = pythonResponse.completions().count { - !it.content().startsWith(userInput) - } - argumentCaptor().apply { - verify(batcher, atLeast(1 + prefixNotMatchCount)).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "result" to Result.Succeeded.toString(), - ) - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - prefixNotMatchCount, - codewhispererSuggestionState to CodewhispererSuggestionState.Discard.toString(), - ) - } - } - - @Test - fun `test getting non-CodeWhisperer Exception will return empty request id`() { - mockClient.stub { - on { - this.generateCompletions(any()) - } doAnswer { - throw Exception("wrong path") - } - } - - invokeCodeWhispererService() - - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "codewhispererRequestId" to "", - "result" to Result.Failed.toString(), - ) - } - } - - @Test - fun `test getting CodeWhispererException will capture request id`() { - mockClient.stub { - on { - this.generateCompletions(any()) - } doAnswer { - throw testCodeWhispererException - } - } - - invokeCodeWhispererService() - - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - assertEventsContainsFieldsAndCount( - allValues, - serviceInvocation, - 1, - "codewhispererRequestId" to testRequestIdForCodeWhispererException, - "result" to Result.Failed.toString(), - ) - } - } - - @Test - fun `test user Decision events record CodeWhisperer reference info`() { - withCodeWhispererServiceInvokedAndWait {} - val mapper = jacksonObjectMapper() - argumentCaptor().apply { - verify(batcher, atLeastOnce()).enqueue(capture()) - pythonResponse.completions().forEach { - assertEventsContainsFieldsAndCount( - allValues, - userDecision, - 1, - "codewhispererSuggestionReferences" to mapper.writeValueAsString(it.references().map { ref -> ref.licenseName() }.toSet()), - "codewhispererSuggestionReferenceCount" to it.references().size.toString(), - atLeast = true - ) - } - } - } - - @Test - fun `test invoking CodeWhisperer will send service invocation event with sessionId and requestId from response`() { - withCodeWhispererServiceInvokedAndWait { states -> - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - serviceInvocation, - 1, - "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, - ) - } - } - - @Test - fun `test userDecision events will record sessionId and requestId from response`() { - val statesCaptor = argumentCaptor() - withCodeWhispererServiceInvokedAndWait {} - verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(statesCaptor.capture(), any(), any(), any()) - val states = statesCaptor.lastValue - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - states.recommendationContext.details.size, - "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, - ) - } - - @Test - fun `test codePercentage tracker will not be activated if CWSPR terms of service is not accepted`() { - val exploreManagerMock = mock { - on { checkActiveCodeWhispererConnectionType(projectRule.project) } doReturn CodeWhispererLoginType.Logout - } - ApplicationManager.getApplication().replaceService(CodeWhispererExplorerActionManager::class.java, exploreManagerMock, disposableRule.disposable) - val project = projectRule.project - val fixture = projectRule.fixture - val tracker = CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE) - assertThat(tracker.isTrackerActive()).isFalse - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString("arbitrary string to trigger documentChanged") - } - } - assertThat(tracker.isTrackerActive()).isFalse - } - - @Test - fun `test codePercentage tracker will not be initialized with unsupportedLanguage`() { - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(0) - val project = projectRule.project - val fixture = projectRule.fixture - val plainTxtFile = fixture.addFileToProject("/notSupported.txt", "") - - runInEdtAndWait { - fixture.openFileInEditor(plainTxtFile.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString("random txt string") - } - } - - // document changes in unsupported files will not trigger initialization of tracker - assertThat(CodeWhispererCodeCoverageTracker.getInstancesMap()).hasSize(0) - } - - @Test - fun `test codePercentage metric is correct - 1`() { - val project = projectRule.project - val fixture = projectRule.fixture - val emptyFile = fixture.addFileToProject("/anotherFile.py", "") - // simulate users typing behavior of the following - // user hit one key stroke - runInEdtAndWait { - fixture.openFileInEditor(emptyFile.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(keystrokeInput) - } - } - // simulate users accepting the recommendation and delete part of the recommendation - // (x, y):\n return x + y - val deletedTokenByUser = 4 - withCodeWhispererServiceInvokedAndWait { - popupManagerSpy.popupComponents.acceptButton.doClick() - } - - runInEdtAndWait { - val offset = fixture.caretOffset - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.document.deleteString(offset - deletedTokenByUser, offset) - fixture.editor.caretModel.moveToOffset(fixture.editor.document.textLength) - } - } - - CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose() - - val rawAcceptedTokenSize = pythonResponse.completions().first().content().length.toLong() - val acceptedTokensSize = rawAcceptedTokenSize - deletedTokenByUser - val totalTokensSize = keystrokeInput.length + rawAcceptedTokenSize - - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - codePercentage, - 1, - "codewhispererAcceptedTokens" to acceptedTokensSize.toString(), - "codewhispererTotalTokens" to totalTokensSize.toString(), - "codewhispererSuggestedTokens" to rawAcceptedTokenSize.toString(), - "codewhispererPercentage" to CodeWhispererCodeCoverageTracker.calculatePercentage(rawAcceptedTokenSize, totalTokensSize).toString(), - ) - } - - @Test - fun `test codePercentage metric is correct - 2`() { - val project = projectRule.project - val fixture = projectRule.fixture - val emptyFile = fixture.addFileToProject("/anotherFile.py", "") - // simulate users typing behavior - runInEdtAndWait { - fixture.openFileInEditor(emptyFile.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(keystrokeInput) - } - } - // simulate users accepting the recommendation - // (x, y):\n return x + y - val anotherKeyStrokeInput = "\n " - withCodeWhispererServiceInvokedAndWait { - popupManagerSpy.popupComponents.acceptButton.doClick() - } - - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(anotherKeyStrokeInput) - val currentOffset = fixture.editor.caretModel.offset - // delete 1 char - fixture.editor.document.deleteString(currentOffset - 1, currentOffset) - } - // use dispose() to force tracker to emit telemetry - CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose() - } - - val rawAcceptedTokenSize = pythonResponse.completions().first().content().length.toLong() - val totalTokensSize = keystrokeInput.length + rawAcceptedTokenSize + 1 - - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - codePercentage, - 1, - "codewhispererAcceptedTokens" to rawAcceptedTokenSize.toString(), - "codewhispererSuggestedTokens" to rawAcceptedTokenSize.toString(), - "codewhispererTotalTokens" to totalTokensSize.toString(), - "codewhispererPercentage" to CodeWhispererCodeCoverageTracker.calculatePercentage(rawAcceptedTokenSize, totalTokensSize).toString(), - ) - } - - /** - * When user accept recommendation in file1, then switch and delete code in file2, if the deletion code in file2 is on pre-existing code, - * we should not decrement totalTokens by the length of the code deleted - */ - @Test - fun `test codePercentage metric - switching files and delete tokens`() { - val project = projectRule.project - val fixture = projectRule.fixture - val emptyFile = fixture.addFileToProject("/anotherFile.py", pythonTestLeftContext) - // simulate users typing behavior - runInEdtAndWait { - fixture.openFileInEditor(emptyFile.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(keystrokeInput) - } - } - - // accept recommendation in file1.py - withCodeWhispererServiceInvokedAndWait { - popupManagerSpy.popupComponents.acceptButton.doClick() - } - - val file2 = fixture.addFileToProject("./file2.py", "Pre-existing string") - // switch to file2.py and delete code there - runInEdtAndWait { - fixture.openFileInEditor(file2.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.document.deleteString(0, file2.textLength) - } - } - - // use dispose() to force tracker to emit telemetry - CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose() - - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - val rawAcceptedTokenCount = pythonResponse.completions().first().content().length - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - codePercentage, - 1, - "codewhispererAcceptedTokens" to rawAcceptedTokenCount.toString(), - "codewhispererSuggestedTokens" to rawAcceptedTokenCount.toString(), - "codewhispererTotalTokens" to (1 + rawAcceptedTokenCount).toString(), - "codewhispererPercentage" to "96", - ) - } - - @Test - fun `test codePercentage metric is correct - simulate IDE adding right closing paren`() { - val project = projectRule.project - val fixture = projectRule.fixture - val emptyFile = fixture.addFileToProject("/anotherFile.py", "") - // simulate users typing behavior of the following - // def addTwoNumbers( - whenever(mockClient.generateCompletions(any())).thenReturn( - GenerateCompletionsResponse.builder() - .completions( - CodeWhispererTestUtil.generateMockCompletionDetail("x, y):\n return x + y"), - CodeWhispererTestUtil.generateMockCompletionDetail("a, b):\n return a + b"), - ) - .nextToken("") - .responseMetadata(DefaultAwsResponseMetadata.create(mapOf(AwsHeader.AWS_REQUEST_ID to testRequestId))) - .sdkHttpResponse(SdkHttpResponse.builder().headers(mapOf(CodeWhispererService.KET_SESSION_ID to listOf(testSessionId))).build()) - .build() as GenerateCompletionsResponse - ) - - runInEdtAndWait { - fixture.openFileInEditor(emptyFile.virtualFile) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString("(") - // add closing paren but not move the caret position, simulating IDE's behavior - fixture.editor.document.insertString(fixture.editor.caretModel.offset, ")") - } - } - - withCodeWhispererServiceInvokedAndWait { - popupManagerSpy.popupComponents.acceptButton.doClick() - } - CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose() - - val rawAcceptedTokenSize = "x, y):\n return x + y".length - val acceptedTokensSize = rawAcceptedTokenSize.toLong() - val totalTokensSize = "()".length + acceptedTokensSize - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - codePercentage, - 1, - "codewhispererAcceptedTokens" to acceptedTokensSize.toString(), - "codewhispererSuggestedTokens" to rawAcceptedTokenSize.toString(), - "codewhispererTotalTokens" to totalTokensSize.toString(), - "codewhispererPercentage" to CodeWhispererCodeCoverageTracker.calculatePercentage(acceptedTokensSize, totalTokensSize).toString(), - ) - } - - @Test - fun `test empty list of recommendations should sent 1 empty userDecision event and no popup shown`() { - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - emptyListResponse - } - } - invokeCodeWhispererService() - - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any()) - runInEdtAndWait { - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - 1, - "codewhispererSuggestionState" to CodewhispererSuggestionState.Empty.toString(), - ) - } - } - - @Test - fun `test getting some recommendations and a final empty list of recommendations at session end should not sent empty userDecision events`() { - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doReturnConsecutively(listOf(pythonResponseWithNonEmptyToken, emptyListResponse)) - } - - withCodeWhispererServiceInvokedAndWait { } - - runInEdtAndWait { - val metricCaptor = argumentCaptor() - // 2 serviceInvocation + 5 userDecision - verify(batcher, atLeast(pythonResponseWithNonEmptyToken.completions().size + 2)).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - 0, - "codewhispererSuggestionState" to CodewhispererSuggestionState.Empty.toString(), - ) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - 1, - "codewhispererSuggestionState" to CodewhispererSuggestionState.Accept.toString(), - ) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - 4, - "codewhispererSuggestionState" to CodewhispererSuggestionState.Unseen.toString(), - ) - } - } - - @Test - fun `test empty recommendations should send empty user decision events`() { - testSendEmptyUserDecisionEventForEmptyRecommendations(listOfEmptyRecommendationResponse) - } - - @Test - fun `test a mix of empty and non-empty recommendations should send empty user decision events accordingly`() { - testSendEmptyUserDecisionEventForEmptyRecommendations(listOfMixedEmptyAndNonEmptyRecommendationResponse) - } - - @Test - fun `test toggle autoSugestion will emit autoSuggestionActivation telemetry (popup)`() { + fun `test toggle autoSuggestion will emit autoSuggestionActivation telemetry (popup)`() { val metricCaptor = argumentCaptor() doNothing().`when`(batcher).enqueue(metricCaptor.capture()) @@ -713,54 +77,11 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { ) } - private fun testSendEmptyUserDecisionEventForEmptyRecommendations(response: GenerateCompletionsResponse) { - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - response - } - } - - invokeCodeWhispererService() - - val numOfEmptyRecommendations = response.completions().filter { it.content().isEmpty() }.size - if (numOfEmptyRecommendations == response.completions().size) { - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any()) - } else { - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000)) - .showPopup(any(), any(), popupCaptor.capture(), any()) - runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) - } - } - runInEdtAndWait { - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - numOfEmptyRecommendations, - "codewhispererSuggestionState" to CodewhispererSuggestionState.Empty.toString(), - "codewhispererCompletionType" to CodewhispererCompletionType.Line.toString() - ) - } - } - - private fun Editor.appendString(string: String) { - val currentOffset = caretModel.primaryCaret.offset - document.insertString(currentOffset, string) - caretModel.moveToOffset(currentOffset + string.length) - PsiDocumentManager.getInstance(projectRule.project).commitDocument(document) - } - @After override fun tearDown() { super.tearDown() telemetryService.dispose() AwsSettings.getInstance().isTelemetryEnabled = isTelemetryEnabledDefault - CodeWhispererCodeCoverageTracker.getInstancesMap().clear() } companion object { 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 e000bbaeafc..c13a89d029a 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 @@ -11,6 +11,8 @@ import com.intellij.testFramework.DisposableRule import com.intellij.testFramework.RuleChain import com.intellij.testFramework.replaceService import com.intellij.testFramework.runInEdtAndWait +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest @@ -27,9 +29,6 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.timeout import org.mockito.kotlin.verify -import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.paginators.GenerateCompletionsIterable import software.amazon.awssdk.services.ssooidc.SsoOidcClient import software.aws.toolkits.jetbrains.core.MockClientManagerRule import software.aws.toolkits.jetbrains.core.credentials.ManagedSsoProfile @@ -37,6 +36,11 @@ 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.lsp.AmazonQLanguageServer +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.WorkspaceInfo +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences 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 @@ -44,6 +48,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestU import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testValidAccessToken import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererRecommendationAction +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager @@ -63,6 +68,7 @@ import software.aws.toolkits.jetbrains.settings.CodeWhispererConfigurationType import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule import software.aws.toolkits.resources.message +import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicReference // TODO: restructure testbase, too bulky and hard to debug @@ -77,11 +83,11 @@ open class CodeWhispererTestBase { @JvmField val ruleChain = RuleChain(projectRule, mockCredentialRule, mockClientManagerRule, authManagerRule, disposableRule) - protected lateinit var mockClient: CodeWhispererRuntimeClient + protected lateinit var mockLspService: AmazonQLspService + protected lateinit var mockLanguageServer: AmazonQLanguageServer protected lateinit var popupManagerSpy: CodeWhispererPopupManager protected lateinit var clientAdaptorSpy: CodeWhispererClientAdaptor - protected lateinit var invocationStatusSpy: CodeWhispererInvocationStatus internal lateinit var stateManager: CodeWhispererExplorerActionManager protected lateinit var recommendationManager: CodeWhispererRecommendationManager protected lateinit var codewhispererService: CodeWhispererService @@ -90,26 +96,19 @@ open class CodeWhispererTestBase { private lateinit var originalExplorerActionState: CodeWhispererExploreActionState private lateinit var originalSettings: CodeWhispererConfiguration private lateinit var qRegionProfileManagerSpy: QRegionProfileManager + protected lateinit var codeScanManager: CodeWhispererCodeScanManager @Before open fun setUp() { - mockClient = mockClientManagerRule.create() + mockLspService = spy(AmazonQLspService.getInstance(projectRule.project)) + mockLanguageServer = mockk() + + // Mock the service methods on Project + projectRule.project.replaceService(AmazonQLspService::class.java, mockLspService, disposableRule.disposable) + mockLspInlineCompletionResponse(pythonResponse) + mockClientManagerRule.create() - val requestCaptor = argumentCaptor() - mockClient.stub { - on { - mockClient.generateCompletionsPaginator(requestCaptor.capture()) - } doAnswer { - GenerateCompletionsIterable(mockClient, requestCaptor.lastValue) - } - } - mockClient.stub { - on { - mockClient.generateCompletions(any()) - } doAnswer { - pythonResponse - } - } + every { mockLanguageServer.logInlineCompletionSessionResults(any()) } returns CompletableFuture.completedFuture(Unit) popupManagerSpy = spy(CodeWhispererPopupManager.getInstance()) popupManagerSpy.reset() @@ -123,19 +122,17 @@ open class CodeWhispererTestBase { } ApplicationManager.getApplication().replaceService(CodeWhispererPopupManager::class.java, popupManagerSpy, disposableRule.disposable) - invocationStatusSpy = spy(CodeWhispererInvocationStatus.getInstance()) - invocationStatusSpy.stub { - on { - hasEnoughDelayToShowCodeWhisperer() + stateManager = spy(CodeWhispererExplorerActionManager.getInstance()) + recommendationManager = CodeWhispererRecommendationManager.getInstance() + codewhispererService = spy(CodeWhispererService.getInstance()) + codewhispererService.stub { + onGeneric { + getWorkspaceIds(any()) } doAnswer { - true + CompletableFuture.completedFuture(LspServerConfigurations(listOf(WorkspaceInfo("file:///", "workspaceId")))) } } - ApplicationManager.getApplication().replaceService(CodeWhispererInvocationStatus::class.java, invocationStatusSpy, disposableRule.disposable) - - stateManager = spy(CodeWhispererExplorerActionManager.getInstance()) - recommendationManager = CodeWhispererRecommendationManager.getInstance() - codewhispererService = CodeWhispererService.getInstance() + ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererService, disposableRule.disposable) editorManager = CodeWhispererEditorManager.getInstance() settingsManager = CodeWhispererSettings.getInstance() @@ -169,6 +166,11 @@ open class CodeWhispererTestBase { ApplicationManager.getApplication().replaceService(CodeWhispererExplorerActionManager::class.java, stateManager, disposableRule.disposable) stateManager.setAutoEnabled(false) + codeScanManager = spy(CodeWhispererCodeScanManager.getInstance(projectRule.project)) + doNothing().`when`(codeScanManager).buildCodeScanUI() + doNothing().`when`(codeScanManager).removeCodeScanUI() + projectRule.project.replaceService(CodeWhispererCodeScanManager::class.java, codeScanManager, disposableRule.disposable) + val conn = authManagerRule.createConnection(ManagedSsoProfile("us-east-1", "url", Q_SCOPES)) ToolkitConnectionManager.getInstance(projectRule.project).switchConnection(conn) @@ -260,13 +262,12 @@ open class CodeWhispererTestBase { } fun addUserInputAfterInvocation(userInput: String) { - val codewhispererServiceSpy = spy(codewhispererService) val triggerTypeCaptor = argumentCaptor() val editorCaptor = argumentCaptor() val projectCaptor = argumentCaptor() val psiFileCaptor = argumentCaptor() val latencyContextCaptor = argumentCaptor() - codewhispererServiceSpy.stub { + codewhispererService.stub { onGeneric { getRequestContext( triggerTypeCaptor.capture(), @@ -276,7 +277,7 @@ open class CodeWhispererTestBase { latencyContextCaptor.capture() ) }.doAnswer { - val requestContext = codewhispererServiceSpy.getRequestContext( + val requestContext = codewhispererService.getRequestContext( triggerTypeCaptor.firstValue, editorCaptor.firstValue, projectCaptor.firstValue, @@ -287,6 +288,15 @@ open class CodeWhispererTestBase { requestContext }.thenCallRealMethod() } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) + } + + fun mockLspInlineCompletionResponse(response: InlineCompletionListWithReferences) { + mockLspService.stub { + onGeneric { + executeSync>(any()) + } doAnswer { + CompletableFuture.completedFuture(response) + } + } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt new file mode 100644 index 00000000000..fd6b9a0c49f --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt @@ -0,0 +1,187 @@ +// 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.editor.Editor +import com.intellij.openapi.project.Project +import org.eclipse.lsp4j.jsonrpc.messages.Either +import software.amazon.awssdk.awscore.DefaultAwsResponseMetadata +import software.amazon.awssdk.awscore.util.AwsHeader +import software.amazon.awssdk.http.SdkHttpResponse +import software.aws.toolkits.core.utils.test.aString +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionImports +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionItem +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReference +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionReferencePosition +import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererC +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCpp +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCsharp +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererGo +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererKotlin +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPhp +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererRuby +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererScala +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererShell +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererSql +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript +import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition +import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo +import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService +import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext +import software.aws.toolkits.telemetry.CodewhispererTriggerType +import java.nio.file.Paths +import kotlin.random.Random + +object CodeWhispererTestUtil { + const val testSessionId = "test_codewhisperer_session_id" + const val testRequestId = "test_aws_request_id" + const val testRequestIdForCodeWhispererException = "test_request_id_for_codewhispererException" + const val codeWhispererRecommendationActionId = "CodeWhispererRecommendationAction" + const val codeWhispererCodeScanActionId = "codewhisperer.toolbar.security.scan" + const val testValidAccessToken = "test_valid_access_token" + val testNextToken: Either = Either.forLeft("") + val metadata: DefaultAwsResponseMetadata = DefaultAwsResponseMetadata.create( + mapOf(AwsHeader.AWS_REQUEST_ID to testRequestId) + ) + val sdkHttpResponse = SdkHttpResponse.builder().headers( + mapOf(CodeWhispererService.KET_SESSION_ID to listOf(testSessionId)) + ).build() + + val pythonResponse: InlineCompletionListWithReferences = InlineCompletionListWithReferences( + items = listOf( + InlineCompletionItem("item1", "(x, y):\n return x + y"), + InlineCompletionItem("item2", "(a, b):\n return a + b"), + InlineCompletionItem("item3", "test recommendation 3"), + InlineCompletionItem("item4", "test recommendation 4"), + InlineCompletionItem("item4", "test recommendation 5"), + ), + sessionId = "sessionId", + partialResultToken = testNextToken + ) + val javaResponse: InlineCompletionListWithReferences = InlineCompletionListWithReferences( + items = listOf( + InlineCompletionItem("item1", "(x, y) {\n return x + y\n }"), + InlineCompletionItem("item2", "(a, b) {\n return a + b\n }"), + InlineCompletionItem("item3", "test recommendation 3"), + InlineCompletionItem("item4", "test recommendation 4"), + InlineCompletionItem("item5", "test recommendation 5"), + ), + sessionId = "sessionId", + partialResultToken = testNextToken + ) + const val pythonFileName = "test.py" + const val javaFileName = "test.java" + const val cppFileName = "test.cpp" + const val jsFileName = "test.js" + const val pythonTestLeftContext = "def addTwoNumbers" + const val keystrokeInput = "a" + const val cppTestLeftContext = "int addTwoNumbers" + const val javaTestContext = "public class Test {\n public static void main\n}" + const val yaml_langauge = "yaml" + const val leftContext_success_Iac = "# Create an S3 Bucket named CodeWhisperer in CloudFormation" + const val leftContext_failure_Iac = "Create an S3 Bucket named CodeWhisperer" +} + +fun aCompletion(content: String? = null, isEmpty: Boolean = false, referenceCount: Int? = null, importCount: Int? = null): InlineCompletionItem { + val myReferenceCount = referenceCount ?: Random.nextInt(0, 4) + val myImportCount = importCount ?: Random.nextInt(0, 4) + + val references = List(myReferenceCount) { + InlineCompletionReference( + aString(), + aString(), + aString(), + InlineCompletionReferencePosition() + ) + } + + val imports = List(myImportCount) { + InlineCompletionImports(aString()) + } + + return InlineCompletionItem( + itemId = aString(), + insertText = content ?: if (!isEmpty) aString() else "", + references = references, + mostRelevantMissingImports = imports + ) +} + +fun aFileContextInfo(language: CodeWhispererProgrammingLanguage? = null): FileContextInfo { + val caretContextInfo = CaretContext(aString(), aString(), aString()) + val fileName = aString() + val fileRelativePath = Paths.get("test", fileName).toString() + val programmingLanguage = language ?: listOf( + CodeWhispererPython.INSTANCE, + CodeWhispererJava.INSTANCE + ).random() + + return FileContextInfo(caretContextInfo, fileName, programmingLanguage, fileRelativePath, null) +} + +fun aTriggerType(): CodewhispererTriggerType = + CodewhispererTriggerType.values().filterNot { it == CodewhispererTriggerType.Unknown }.random() + +fun aRequestContext( + project: Project, + editor: Editor, + myFileContextInfo: FileContextInfo? = null, +): RequestContext { + val triggerType = aTriggerType() + val automatedTriggerType = if (triggerType == CodewhispererTriggerType.AutoTrigger) { + listOf( + CodeWhispererAutomatedTriggerType.IdleTime(), + CodeWhispererAutomatedTriggerType.Enter(), + CodeWhispererAutomatedTriggerType.SpecialChar('a'), + CodeWhispererAutomatedTriggerType.IntelliSense() + ).random() + } else { + CodeWhispererAutomatedTriggerType.Unknown() + } + + return RequestContext( + project, + editor, + TriggerTypeInfo(triggerType, automatedTriggerType), + CaretPosition(Random.nextInt(), Random.nextInt()), + fileContextInfo = myFileContextInfo ?: aFileContextInfo(), + null, + LatencyContext( + Random.nextDouble(), + Random.nextLong(), + Random.nextLong(), + aString() + ), + customizationArn = null, + workspaceId = null, + ) +} + +fun aProgrammingLanguage(): CodeWhispererProgrammingLanguage = listOf( + CodeWhispererJava.INSTANCE, + CodeWhispererPython.INSTANCE, + CodeWhispererJavaScript.INSTANCE, + CodeWhispererTypeScript.INSTANCE, + CodeWhispererJsx.INSTANCE, + CodeWhispererCsharp.INSTANCE, + CodeWhispererKotlin.INSTANCE, + CodeWhispererC.INSTANCE, + CodeWhispererCpp.INSTANCE, + CodeWhispererGo.INSTANCE, + CodeWhispererPhp.INSTANCE, + CodeWhispererRuby.INSTANCE, + CodeWhispererScala.INSTANCE, + CodeWhispererShell.INSTANCE, + CodeWhispererSql.INSTANCE +).random() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt index 87aa3766922..ae2cb710687 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt @@ -85,7 +85,7 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() { projectRule.fixture.editor.caretModel.moveToOffset(pythonTestLeftContext.length) } withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + val recommendation = states.recommendationContext.details[0].completion.insertText val editor = projectRule.fixture.editor val startOffset = editor.caretModel.offset recommendation.forEachIndexed { index, char -> diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt index 674ada12c2f..abb05f74778 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt @@ -27,7 +27,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaFileName -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener @@ -99,18 +98,6 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { } } - @Test - fun `test hitting enter after non-whitespace characters should trigger CodeWhisperer`() { - testHittingEnterAfterWhitespaceCharsShouldTriggerCodeWhisperer(pythonTestLeftContext, 1) - } - - @Test - fun `test hitting enter after whitespace characters should trigger CodeWhisperer`() { - testHittingEnterAfterWhitespaceCharsShouldTriggerCodeWhisperer("$pythonTestLeftContext ", 1) - testHittingEnterAfterWhitespaceCharsShouldTriggerCodeWhisperer("$pythonTestLeftContext\t", 2) - testHittingEnterAfterWhitespaceCharsShouldTriggerCodeWhisperer("$pythonTestLeftContext\n", 3) - } - @Test fun `test hitting enter inside braces in Java file should auto-trigger CodeWhisperer and keep the formatting correct`() { val testLeftContext = "public class Test {\n public static void main() {" @@ -144,46 +131,4 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { verify(popupManagerSpy, times(2)).showPopup(any(), any(), any(), any()) } } - - @Test - fun `test special characters should not trigger CodeWhisperer for user group when there is immediate right context`() { - testInputSpecialCharWithRightContext("add", false) - } - - @Test - fun `test special characters should trigger CodeWhisperer for user group when it is not immediate or is single }`() { - testInputSpecialCharWithRightContext("}", true) - testInputSpecialCharWithRightContext(")", true) - testInputSpecialCharWithRightContext(" add", true) - testInputSpecialCharWithRightContext("\nadd", true) - } - - private fun testInputSpecialCharWithRightContext(rightContext: String, shouldtrigger: Boolean) { - CodeWhispererExplorerActionManager.getInstance().setAutoEnabled(true) - setFileContext(pythonFileName, "def", rightContext) - projectRule.fixture.type('{') - if (shouldtrigger) { - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000).atLeastOnce()) - .showPopup(any(), any(), popupCaptor.capture(), any()) - runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) - } - } else { - verify(popupManagerSpy, times(0)) - .showPopup(any(), any(), any(), any()) - } - } - - private fun testHittingEnterAfterWhitespaceCharsShouldTriggerCodeWhisperer(prompt: String, times: Int) { - CodeWhispererExplorerActionManager.getInstance().setAutoEnabled(true) - setFileContext(pythonFileName, prompt, "") - projectRule.fixture.type('\n') - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000).atLeast(times)) - .showPopup(any(), any(), popupCaptor.capture(), any()) - runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) - } - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt index c4617d4c991..b2e086dc4ef 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt @@ -15,9 +15,9 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { withCodeWhispererServiceInvokedAndWait { states -> val actualRecommendations = states.recommendationContext.details.map { - it.recommendation.content() + it.completion.insertText } - assertThat(actualRecommendations).isEqualTo(pythonResponse.completions().map { it.content() }) + assertThat(actualRecommendations).isEqualTo(pythonResponse.items.map { it.insertText }) } } @@ -26,13 +26,13 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { val userInput = "test" addUserInputAfterInvocation(userInput) - val expectedRecommendations = pythonResponse.completions().map { it.content() } + val expectedRecommendations = pythonResponse.items.map { it.insertText } withCodeWhispererServiceInvokedAndWait { states -> - val actualRecommendations = states.recommendationContext.details.map { it.recommendation.content() } + val actualRecommendations = states.recommendationContext.details.map { it.completion.insertText } assertThat(actualRecommendations).isEqualTo(expectedRecommendations) states.recommendationContext.details.forEachIndexed { index, context -> - val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) + val expectedDiscarded = !pythonResponse.items[index].insertText.startsWith(userInput) val actualDiscarded = context.isDiscarded assertThat(actualDiscarded).isEqualTo(expectedDiscarded) } @@ -51,37 +51,7 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead) states.recommendationContext.details.forEachIndexed { index, actualContext -> val actualDiscarded = actualContext.isDiscarded - val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput + typeahead) - assertThat(actualDiscarded).isEqualTo(expectedDiscarded) - } - } - } - - @Test - fun `test have blank user input should show that all recommendations are valid`() { - val blankUserInput = " " - addUserInputAfterInvocation(blankUserInput) - val userInput = blankUserInput.trimStart() - - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { _, actualContext -> - assertThat(actualContext.isDiscarded).isEqualTo(false) - } - } - } - - @Test - fun `test have user input with leading spaces and matching suffix should show recommendations prefix-matching suffix are valid`() { - val userInputWithLeadingSpaces = " test" - addUserInputAfterInvocation(userInputWithLeadingSpaces) - val userInput = userInputWithLeadingSpaces.trimStart() - - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { index, actualContext -> - val actualDiscarded = actualContext.isDiscarded - val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) + val expectedDiscarded = !pythonResponse.items[index].insertText.startsWith(userInput + typeahead) assertThat(actualDiscarded).isEqualTo(expectedDiscarded) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserModificationTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserModificationTrackerTest.kt deleted file mode 100644 index 0565e54152f..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserModificationTrackerTest.kt +++ /dev/null @@ -1,237 +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 - -import com.intellij.ide.highlighter.JavaFileType -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.testFramework.ProjectExtension -import com.intellij.testFramework.junit5.TestDisposable -import com.intellij.testFramework.replaceService -import info.debatty.java.stringsimilarity.Levenshtein -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.RegisterExtension -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.stub -import org.mockito.kotlin.verify -import software.amazon.awssdk.awscore.DefaultAwsResponseMetadata -import software.amazon.awssdk.awscore.util.AwsHeader -import software.amazon.awssdk.http.SdkHttpResponse -import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse -import software.aws.toolkits.core.telemetry.MetricEvent -import software.aws.toolkits.core.telemetry.TelemetryBatcher -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.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeInsertionDiff -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.percentage -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.InsertedCodeModificationEntry -import software.aws.toolkits.jetbrains.services.telemetry.MockTelemetryServiceExtension -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings -import java.time.Instant -import kotlin.math.min -import kotlin.test.assertNotNull - -class CodeWhispererUserModificationTrackerTest { - @TestDisposable - private lateinit var disposable: Disposable - - @JvmField - @RegisterExtension - val mockTelemetryService = MockTelemetryServiceExtension() - - // sut - private lateinit var sut: CodeWhispererUserModificationTracker - - // dependencies - private lateinit var mockBatcher: TelemetryBatcher - private lateinit var mockClient: CodeWhispererClientAdaptor - private lateinit var mockModelConfigurator: CodeWhispererModelConfigurator - private val project: Project - get() = projectExtension.project - private val levenshtein = Levenshtein() - - companion object { - @JvmField - @RegisterExtension - val projectExtension = ProjectExtension() - private const val customizationArn = "customizationArn" - private const val steRequestId = "sendTelemetryEventRequestId" - private const val conversationId = "conversationId" - private const val messageId = "messageId" - private val mockCustomization = CodeWhispererCustomization(customizationArn, "name", "description") - private val mockSteResponse = SendTelemetryEventResponse.builder() - .apply { - this.sdkHttpResponse( - SdkHttpResponse.builder().build() - ) - this.responseMetadata( - DefaultAwsResponseMetadata.create( - mapOf(AwsHeader.AWS_REQUEST_ID to steRequestId) - ) - ) - }.build() - } - - @BeforeEach - fun setup() { - sut = CodeWhispererUserModificationTracker(project) - - // set up telemetry service - mockBatcher = mockTelemetryService.batcher() - - // set up client - mockClient = mock() - project.replaceService(CodeWhispererClientAdaptor::class.java, mockClient, disposable) - - // set up customization - mockModelConfigurator = mock { - on { activeCustomization(project) } doReturn mockCustomization - } - ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfigurator, disposable) - } - - @Test - fun `sendModificationWithChatTelemetry`() { - mockClient.stub { - on { - sendChatUserModificationTelemetry(any(), any(), any(), any(), any(), any()) - } doReturn mockSteResponse - } - - // TODO: should use real project fixture, fix later - val rangeMarker = mock { - on { startOffset } doReturn 0 - on { endOffset } doReturn 10 - } - val fileMock = mock { - on { isValid } doReturn true - on { extension } doReturn "java" - on { fileType } doReturn JavaFileType.INSTANCE - } - val insertedCodeModificationEntry = InsertedCodeModificationEntry( - conversationId = conversationId, - messageId = messageId, - Instant.now().minusSeconds(301L), - fileMock, - rangeMarker, - "print" - ) - - val textRange = TextRange(rangeMarker.startOffset, rangeMarker.endOffset) - val mockDocument = mock { - on { getText(eq(textRange)) } doReturn "println();" - } - val documentManager = mock { - on { getDocument(fileMock) } doReturn mockDocument - } - ApplicationManager.getApplication().replaceService(FileDocumentManager::class.java, documentManager, disposable) - - sut.enqueue(insertedCodeModificationEntry) - sut.dispose() - - val percentageChanges = sut.checkDiff("println();", "print").percentage() - verify(mockClient).sendChatUserModificationTelemetry( - eq(conversationId), - eq(messageId), - eq(CodeWhispererJava.INSTANCE), - eq(percentageChanges), - eq(CodeWhispererSettings.getInstance().isProjectContextEnabled()), - eq(mockCustomization) - ) - - argumentCaptor { - verify(mockBatcher).enqueue(capture()) - val event = firstValue.data.find { it.name == "amazonq_modifyCode" } - assertNotNull(event) - assertThat(event) - .matches({ it.metadata["cwsprChatConversationId"] == conversationId }, "cwsprChatConversationId doesn't match") - .matches({ it.metadata["cwsprChatMessageId"] == messageId }, "cwsprChatMessageId doesn't match") - .matches( - { it.metadata["cwsprChatModificationPercentage"] == percentageChanges.toString() }, - "cwsprChatModificationPercentage doesn't match" - ) - } - } - - @Test - fun `checkDiff edge cases`() { - // any empty string will return null - val r1 = sut.checkDiff("", "") - assertThat(r1).isNull() - - val r2 = sut.checkDiff("foo", "") - assertThat(r2).isNull() - - val r3 = sut.checkDiff("", "foo") - assertThat(r3).isNull() - - // null will return null - val r4 = sut.checkDiff(null, null) - assertThat(r4).isNull() - - val r5 = sut.checkDiff(null, "foo") - assertThat(r5).isNull() - - val r6 = sut.checkDiff("foo", null) - assertThat(r6).isNull() - } - - @Test - fun `checkDiff should return data having correct payload`() { - val r1 = sut.checkDiff("foo", "bar") - assertThat(r1).isEqualTo( - CodeInsertionDiff(modified = "foo", original = "bar", diff = levenshtein.distance("foo", "bar")) - ) - - val r2 = sut.checkDiff("foo", "foo") - assertThat(r2).isEqualTo( - CodeInsertionDiff(modified = "foo", original = "foo", diff = levenshtein.distance("foo", "foo")) - ) - } - - @Test - fun `CodeInsertionDiff_percentage() should return correct result`() { - fun assertPercentageCorrect(original: String?, modified: String?) { - val diff = sut.checkDiff(currString = modified, acceptedString = original) - val expectedPercentage: Double = when { - original == null || modified == null -> 1.0 - - original.isEmpty() || modified.isEmpty() -> 1.0 - - else -> min(1.0, (levenshtein.distance(modified, original) / original.length)) - } - - val actual = diff.percentage() - - assertThat(actual).isEqualTo(expectedPercentage) - } - - assertPercentageCorrect(null, null) - assertPercentageCorrect(null, "foo") - assertPercentageCorrect("foo", null) - - assertPercentageCorrect("", "") - assertPercentageCorrect("", "foo") - assertPercentageCorrect("foo", "") - - assertPercentageCorrect("foo", "bar") - assertPercentageCorrect("foo", "foo") - assertPercentageCorrect("bar", "foo") - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUtilTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUtilTest.kt index e932e59e821..e386649cbdb 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUtilTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUtilTest.kt @@ -3,7 +3,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer -import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.util.SimpleModificationTracker import com.intellij.testFramework.fixtures.CodeInsightTestFixture import kotlinx.coroutines.runBlocking @@ -13,10 +12,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.codewhispererruntime.model.IdeDiagnostic import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference -import software.amazon.awssdk.services.codewhispererruntime.model.Position -import software.amazon.awssdk.services.codewhispererruntime.model.Range import software.amazon.awssdk.services.ssooidc.SsoOidcClient import software.aws.toolkits.core.utils.test.aStringWithLineCount import software.aws.toolkits.jetbrains.core.MockClientManagerRule @@ -27,10 +23,6 @@ import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getUnmodifiedAcceptedCharsCount -import software.aws.toolkits.jetbrains.services.codewhisperer.util.convertSeverity -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticDifferences -import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticsType import software.aws.toolkits.jetbrains.services.codewhisperer.util.isWithin import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled import software.aws.toolkits.jetbrains.services.codewhisperer.util.toCodeChunk @@ -251,50 +243,6 @@ class CodeWhispererUtilTest { assertThat(getTelemetryOptOutPreference()).isEqualTo(OptOutPreference.OPTOUT) } - @Test - fun `test getUnmodifiedAcceptedCharsCount()`() { - var originalRecommendation = "foo" - var modifiedRecommendation = "fou" - var unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo(2) - - originalRecommendation = "foo" - modifiedRecommendation = "f11111oo" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo(3) - - originalRecommendation = "foo" - modifiedRecommendation = "fo" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo(2) - - originalRecommendation = "helloworld" - modifiedRecommendation = "HelloWorld" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo("helloworld".length - 2) - - originalRecommendation = "helloworld" - modifiedRecommendation = "World" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo("helloworld".length - "hello".length - 1) - - originalRecommendation = "CodeWhisperer" - modifiedRecommendation = "CODE" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo(1) - - originalRecommendation = "CodeWhisperer" - modifiedRecommendation = "codewhispererISBEST" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo("CodeWhisperer".length - 2) - - val pythonCommentAddedByUser = "\"\"\"we don't count this comment as generated by CodeWhisperer\"\"\"\n" - originalRecommendation = "x, y):\n\treturn x + y" - modifiedRecommendation = "x, y):\n$pythonCommentAddedByUser\treturn x + y" - unmodifiedCharsCount = getUnmodifiedAcceptedCharsCount(originalRecommendation, modifiedRecommendation) - assertThat(unmodifiedCharsCount).isEqualTo(originalRecommendation.length) - } - @Test fun `test isWithin() returns true if file is within the given directory`() { val projectRoot = fixture.tempDirFixture.findOrCreateDir("workspace/projectA") @@ -315,125 +263,4 @@ class CodeWhispererUtilTest { val file = fixture.addFileToProject("workspace/projectA1/src/Sample.java", "").virtualFile assertThat(file.isWithin(projectRoot)).isFalse() } - - @Test - fun `getDiagnosticsType correctly identifies syntax errors`() { - val messages = listOf( - "Expected semicolon at end of line", - "Incorrect indent level", - "Syntax error in expression" - ) - - messages.forEach { message -> - assertThat(getDiagnosticsType(message)).isEqualTo("SYNTAX_ERROR") - } - } - - @Test - fun `getDiagnosticsType correctly identifies type errors`() { - val messages = listOf( - "Cannot cast String to Int", - "Type mismatch: expected String but got Int" - ) - - messages.forEach { message -> - assertThat(getDiagnosticsType(message)).isEqualTo("TYPE_ERROR") - } - } - - @Test - fun `getDiagnosticsType returns OTHER for unrecognized patterns`() { - val message = "Some random message" - assertThat(getDiagnosticsType(message)).isEqualTo("OTHER") - } - - @Test - fun `convertSeverity correctly maps severity levels`() { - assertThat(convertSeverity(HighlightSeverity.ERROR)).isEqualTo("ERROR") - assertThat(convertSeverity(HighlightSeverity.WARNING)).isEqualTo("WARNING") - assertThat(convertSeverity(HighlightSeverity.INFORMATION)).isEqualTo("INFORMATION") - assertThat(convertSeverity(HighlightSeverity.INFO)).isEqualTo("INFORMATION") - } - - @Test - fun `getDiagnosticDifferences correctly identifies added and removed diagnostics`() { - val diagnostic1 = IdeDiagnostic.builder() - .ideDiagnosticType("SYNTAX_ERROR") - .severity("ERROR") - .source("inspection1") - .range( - Range.builder() - .start(Position.builder().line(0).character(0).build()) - .end(Position.builder().line(0).character(10).build()) - .build() - ) - .build() - - val diagnostic2 = IdeDiagnostic.builder() - .ideDiagnosticType("TYPE_ERROR") - .severity("WARNING") - .source("inspection2") - .range( - Range.builder() - .start(Position.builder().line(1).character(0).build()) - .end(Position.builder().line(1).character(10).build()) - .build() - ) - .build() - - val oldList = listOf(diagnostic1) - val newList = listOf(diagnostic2) - - val differences = getDiagnosticDifferences(oldList, newList) - - assertThat(differences.added).containsExactly(diagnostic2) - assertThat(differences.removed).containsExactly(diagnostic1) - } - - @Test - fun `getDiagnosticDifferences handles empty lists`() { - val diagnostic = IdeDiagnostic.builder() - .ideDiagnosticType("SYNTAX_ERROR") - .severity("ERROR") - .source("inspection1") - .range( - Range.builder() - .start(Position.builder().line(0).character(0).build()) - .end(Position.builder().line(0).character(10).build()) - .build() - ) - .build() - - val emptyList = emptyList() - val nonEmptyList = listOf(diagnostic) - - val differencesWithEmptyOld = getDiagnosticDifferences(emptyList, nonEmptyList) - assertThat(differencesWithEmptyOld.added).containsExactly(diagnostic) - assertThat(differencesWithEmptyOld.removed).isEmpty() - - val differencesWithEmptyNew = getDiagnosticDifferences(nonEmptyList, emptyList) - assertThat(differencesWithEmptyNew.added).isEmpty() - assertThat(differencesWithEmptyNew.removed).containsExactly(diagnostic) - } - - @Test - fun `getDiagnosticDifferences handles identical lists`() { - val diagnostic = IdeDiagnostic.builder() - .ideDiagnosticType("SYNTAX_ERROR") - .severity("ERROR") - .source("inspection1") - .range( - Range.builder() - .start(Position.builder().line(0).character(0).build()) - .end(Position.builder().line(0).character(10).build()) - .build() - ) - .build() - - val list = listOf(diagnostic) - val differences = getDiagnosticDifferences(list, list) - - assertThat(differences.added).isEmpty() - assertThat(differences.removed).isEmpty() - } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt deleted file mode 100644 index c01b70025de..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2022 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.application.ApplicationManager -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.fixtures.CodeInsightTestFixture -import com.intellij.testFramework.replaceService -import com.intellij.testFramework.runInEdtAndWait -import org.assertj.core.api.Assertions.assertThat -import org.junit.After -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 software.aws.toolkits.core.telemetry.TelemetryBatcher -import software.aws.toolkits.core.telemetry.TelemetryPublisher -import software.aws.toolkits.jetbrains.core.MockClientManagerRule -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName -import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType -import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker.Companion.Q_FEATURE_TOPIC -import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher -import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService -import software.aws.toolkits.jetbrains.settings.AwsSettings -import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule - -internal class UserWrittenCodeTrackerTest { - - internal class TestTelemetryService( - publisher: TelemetryPublisher = NoOpPublisher(), - batcher: TelemetryBatcher, - ) : TelemetryService(publisher, batcher) - - @Rule - @JvmField - val disposableRule = DisposableRule() - - @Rule - @JvmField - val mockClientManagerRule = MockClientManagerRule() - - @Rule - @JvmField - var projectRule = PythonCodeInsightTestFixtureRule() - - lateinit var project: Project - lateinit var fixture: CodeInsightTestFixture - lateinit var telemetryServiceSpy: TelemetryService - lateinit var batcher: TelemetryBatcher - lateinit var exploreActionManagerMock: CodeWhispererExplorerActionManager - lateinit var sut: UserWrittenCodeTracker - - @Before - open fun setup() { - this.project = projectRule.project - this.fixture = projectRule.fixture - fixture.configureByText(pythonFileName, pythonTestLeftContext) - AwsSettings.getInstance().isTelemetryEnabled = true - batcher = mock() - - exploreActionManagerMock = mock { - on { checkActiveCodeWhispererConnectionType(any()) } doReturn CodeWhispererLoginType.Sono - } - - ApplicationManager.getApplication().replaceService(CodeWhispererExplorerActionManager::class.java, exploreActionManagerMock, disposableRule.disposable) - - fixture.configureByText(pythonFileName, pythonTestLeftContext) - runInEdtAndWait { - projectRule.fixture.editor.caretModel.primaryCaret.moveToOffset(projectRule.fixture.editor.document.textLength) - } - } - - @After - fun tearDown() { - if (::sut.isInitialized) { - sut.forceTrackerFlush() - sut.reset() - } - } - - @Test - fun `test tracker is listening to q service invocation`() { - sut = UserWrittenCodeTracker.getInstance(project) - sut.activateTrackerIfNotActive() - assertThat(sut.qInvocationCount.get()).isEqualTo(0) - ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.INVOCATION) - assertThat(sut.qInvocationCount.get()).isEqualTo(1) - ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.INVOCATION) - assertThat(sut.qInvocationCount.get()).isEqualTo(2) - } - - @Test - fun `test tracker is not listening to multi char input more than 50, but works for less than 50, and will not increment totalTokens - add new code`() { - sut = UserWrittenCodeTracker.getInstance(project) - sut.activateTrackerIfNotActive() - fixture.configureByText(pythonFileName, "") - val newCode = "def addTwoNumbers\n return" - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(newCode) - } - } - val language: CodeWhispererProgrammingLanguage = CodeWhispererPython.INSTANCE - assertThat(sut.userWrittenCodeCharacterCount[language]).isEqualTo(newCode.length.toLong()) - assertThat(sut.userWrittenCodeLineCount[language]).isEqualTo(1) - - val anotherCode = "(x, y):\n".repeat(8) - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(anotherCode) - } - } - assertThat(sut.userWrittenCodeCharacterCount[language]).isEqualTo(newCode.length.toLong()) - assertThat(sut.userWrittenCodeLineCount[language]).isEqualTo(1) - } - - @Test - fun `test tracker is listening to document changes and increment totalTokens - delete code should not affect`() { - sut = UserWrittenCodeTracker.getInstance(project) - sut.activateTrackerIfNotActive() - assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(CodeWhispererPython.INSTANCE, 0)).isEqualTo(0) - runInEdtAndWait { - fixture.editor.caretModel.primaryCaret.moveToOffset(fixture.editor.document.textLength) - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.document.deleteString(fixture.editor.caretModel.offset - 3, fixture.editor.caretModel.offset) - } - } - assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(CodeWhispererPython.INSTANCE, 0)).isEqualTo(0) - } - - @Test - fun `test tracker is listening to document changes only when Q is not editing`() { - sut = UserWrittenCodeTracker.getInstance(project) - sut.activateTrackerIfNotActive() - fixture.configureByText(pythonFileName, "") - val newCode = "def addTwoNumbers\n return" - - ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.STARTS_EDITING) - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(newCode) - } - } - - ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.FINISHES_EDITING) - val language: CodeWhispererProgrammingLanguage = CodeWhispererPython.INSTANCE - assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(language, 0)).isEqualTo(0) - assertThat(sut.userWrittenCodeLineCount.getOrDefault(language, 0)).isEqualTo(0) - - runInEdtAndWait { - WriteCommandAction.runWriteCommandAction(project) { - fixture.editor.appendString(newCode) - } - } - assertThat(sut.userWrittenCodeCharacterCount[CodeWhispererPython.INSTANCE]).isEqualTo(newCode.length.toLong()) - assertThat(sut.userWrittenCodeLineCount[CodeWhispererPython.INSTANCE]).isEqualTo(1) - } - - private fun Editor.appendString(string: String) { - val currentOffset = caretModel.primaryCaret.offset - document.insertString(currentOffset, string) - caretModel.moveToOffset(currentOffset + string.length) - } -} 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 deleted file mode 100644 index bd40ed11397..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright 2022 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.editor.VisualPosition -import com.intellij.openapi.project.Project -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.mockito.kotlin.mock -import software.amazon.awssdk.awscore.DefaultAwsResponseMetadata -import software.amazon.awssdk.awscore.exception.AwsErrorDetails -import software.amazon.awssdk.awscore.util.AwsHeader -import software.amazon.awssdk.http.SdkHttpResponse -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException -import software.amazon.awssdk.services.codewhispererruntime.model.Completion -import software.amazon.awssdk.services.codewhispererruntime.model.FileContext -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse -import software.amazon.awssdk.services.codewhispererruntime.model.Import -import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage -import software.amazon.awssdk.services.codewhispererruntime.model.RecommendationsWithReferencesPreference -import software.amazon.awssdk.services.codewhispererruntime.model.Reference -import software.amazon.awssdk.services.codewhispererruntime.model.Span -import software.aws.toolkits.core.utils.test.aString -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererC -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCpp -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCsharp -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererGo -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererKotlin -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPhp -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererRuby -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererScala -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererShell -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererSql -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript -import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition -import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy -import software.aws.toolkits.jetbrains.services.codewhisperer.util.UtgStrategy -import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import software.aws.toolkits.telemetry.CodewhispererTriggerType -import java.nio.file.Paths -import kotlin.random.Random - -object CodeWhispererTestUtil { - const val testSessionId = "test_codewhisperer_session_id" - const val testRequestId = "test_aws_request_id" - const val testRequestIdForCodeWhispererException = "test_request_id_for_codewhispererException" - const val codeWhispererRecommendationActionId = "CodeWhispererRecommendationAction" - const val codeWhispererCodeScanActionId = "codewhisperer.toolbar.security.scan" - const val testValidAccessToken = "test_valid_access_token" - const val testNextToken = "test_next_token" - private val testReferenceInfoPair = listOf( - Pair("MIT", "testRepo1"), - Pair("Apache-2.0", "testRepo2"), - Pair("BSD-4-Clause", "testRepo3") - ) - val metadata: DefaultAwsResponseMetadata = DefaultAwsResponseMetadata.create( - mapOf(AwsHeader.AWS_REQUEST_ID to testRequestId) - ) - val sdkHttpResponse = SdkHttpResponse.builder().headers( - mapOf(CodeWhispererService.KET_SESSION_ID to listOf(testSessionId)) - ).build() - private val errorDetail = AwsErrorDetails.builder() - .errorCode("123") - .errorMessage("something went wrong") - .sdkHttpResponse(sdkHttpResponse) - .build() - val testCodeWhispererException = CodeWhispererRuntimeException.builder() - .requestId(testRequestIdForCodeWhispererException) - .awsErrorDetails(errorDetail) - .build() as CodeWhispererRuntimeException - - val pythonRequest: GenerateCompletionsRequest = GenerateCompletionsRequest.builder() - .fileContext( - FileContext.builder() - .filename("test.py") - .programmingLanguage( - ProgrammingLanguage.builder() - .languageName("python") - .build() - ) - .build() - ) - .nextToken("") - .referenceTrackerConfiguration { it.recommendationsWithReferences(RecommendationsWithReferencesPreference.ALLOW) } - .maxResults(5) - .build() - - val pythonResponse: GenerateCompletionsResponse = GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail("(x, y):\n return x + y"), - generateMockCompletionDetail("(a, b):\n return a + b"), - generateMockCompletionDetail("test recommendation 3"), - generateMockCompletionDetail("test recommendation 4"), - generateMockCompletionDetail("test recommendation 5") - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - val pythonResponseWithNonEmptyToken = pythonResponseWithToken(testNextToken) - val javaResponse: GenerateCompletionsResponse = GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail("(x, y) {\n return x + y\n }"), - generateMockCompletionDetail("(a, b) {\n return a + b\n }"), - generateMockCompletionDetail("test recommendation 3"), - generateMockCompletionDetail("test recommendation 4"), - generateMockCompletionDetail("test recommendation 5") - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - val emptyListResponse: GenerateCompletionsResponse = GenerateCompletionsResponse.builder() - .completions(listOf()) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - val listOfEmptyRecommendationResponse: GenerateCompletionsResponse = GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail(""), - generateMockCompletionDetail(""), - generateMockCompletionDetail(""), - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - val listOfMixedEmptyAndNonEmptyRecommendationResponse: GenerateCompletionsResponse = GenerateCompletionsResponse.builder() - .completions( - generateMockCompletionDetail(""), - generateMockCompletionDetail("test recommendation 3"), - generateMockCompletionDetail(""), - generateMockCompletionDetail("test recommendation 4"), - generateMockCompletionDetail("test recommendation 5") - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as GenerateCompletionsResponse - - const val pythonFileName = "test.py" - const val javaFileName = "test.java" - const val cppFileName = "test.cpp" - const val jsFileName = "test.js" - const val pythonTestLeftContext = "def addTwoNumbers" - const val keystrokeInput = "a" - const val cppTestLeftContext = "int addTwoNumbers" - const val javaTestContext = "public class Test {\n public static void main\n}" - const val yaml_langauge = "yaml" - const val leftContext_success_Iac = "# Create an S3 Bucket named CodeWhisperer in CloudFormation" - const val leftContext_failure_Iac = "Create an S3 Bucket named CodeWhisperer" - - fun pythonResponseWithToken(token: String): GenerateCompletionsResponse = - pythonResponse.toBuilder().nextToken(token).build() - - fun generateMockCompletionDetail(content: String): Completion { - val referenceInfo = getReferenceInfo() - return Completion.builder().content(content) - .references( - generateMockReferences(referenceInfo.first, referenceInfo.second, 0, content.length) - ) - .build() - } - - fun getReferenceInfo() = testReferenceInfoPair[Random.nextInt(testReferenceInfoPair.size)] - - fun generateMockCompletionDetail( - content: String, - licenseName: String, - repository: String, - start: Int, - end: Int, - ): Completion = - Completion.builder() - .content(content) - .references(generateMockReferences(licenseName, repository, start, end)) - .build() - - private fun generateMockReferences(licenseName: String, repository: String, start: Int, end: Int) = - Reference.builder() - .licenseName(licenseName) - .repository(repository) - .recommendationContentSpan( - Span.builder() - .start(start) - .end(end) - .build() - ) - .build() -} - -fun aRequestContext( - project: Project, - myFileContextInfo: FileContextInfo? = null, - mySupplementalContextInfo: SupplementalContextInfo? = null, -): RequestContext { - val triggerType = aTriggerType() - val automatedTriggerType = if (triggerType == CodewhispererTriggerType.AutoTrigger) { - listOf( - CodeWhispererAutomatedTriggerType.IdleTime(), - CodeWhispererAutomatedTriggerType.Enter(), - CodeWhispererAutomatedTriggerType.SpecialChar('a'), - CodeWhispererAutomatedTriggerType.IntelliSense() - ).random() - } else { - CodeWhispererAutomatedTriggerType.Unknown() - } - - val supplementalContextDeferred = runBlocking { - async { - mySupplementalContextInfo ?: aSupplementalContextInfo() - } - } - - return RequestContext( - project, - mock(), - TriggerTypeInfo(triggerType, automatedTriggerType), - CaretPosition(Random.nextInt(), Random.nextInt()), - fileContextInfo = myFileContextInfo ?: aFileContextInfo(), - supplementalContextDeferred = supplementalContextDeferred, - null, - LatencyContext( - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextDouble(), - Random.nextDouble(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - aString() - ), - customizationArn = null, - profileArn = null, - workspaceId = null, - diagnostics = emptyList() - ) -} - -fun aSupplementalContextInfo(myContents: List? = null, myIsUtg: Boolean? = null, myLatency: Long? = null): SupplementalContextInfo { - val contents = mutableListOf() - val numberOfContent = Random.nextInt(1, 4) - repeat(numberOfContent) { - contents.add( - Chunk( - content = aString(), - path = aString(), - ) - ) - } - - val isUtg = Random.nextBoolean() - val latency = Random.nextLong(from = 0L, until = 100L) - - return SupplementalContextInfo( - isUtg = myIsUtg ?: isUtg, - latency = myLatency ?: latency, - contents = myContents ?: contents, - targetFileName = aString(), - strategy = if (myIsUtg ?: isUtg) UtgStrategy.ByName else CrossFileStrategy.OpenTabsBM25 - ) -} - -fun aRecommendationContext(): RecommendationContext { - val details = mutableListOf() - val size = Random.nextInt(1, 5) - for (i in 1..size) { - details.add( - i - 1, - DetailContext( - aString(), - aCompletion(), - aCompletion(), - listOf(true, false).random(), - listOf(true, false).random(), - aString(), - CodewhispererCompletionType.Line - ) - ) - } - - return RecommendationContext( - details, - aString(), - aString(), - VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)) - ) -} - -/** - * util to generate a RecommendationContext and a SessionContext given expected decisions - */ -fun aRecommendationContextAndSessionContext(decisions: List): Pair { - val table = CodewhispererSuggestionState.values().associateWith { 0 }.toMutableMap() - decisions.forEach { - table[it]?.let { curCount -> table[it] = 1 + curCount } - } - - val details = mutableListOf() - decisions.forEach { decision -> - val toAdd = if (decision == CodewhispererSuggestionState.Empty) { - val completion = aCompletion("", true, 0, 0) - DetailContext(aString(), completion, completion, Random.nextBoolean(), Random.nextBoolean(), aString(), CodewhispererCompletionType.Line) - } else if (decision == CodewhispererSuggestionState.Discard) { - val completion = aCompletion() - DetailContext(aString(), completion, completion, true, Random.nextBoolean(), aString(), CodewhispererCompletionType.Line) - } else { - val completion = aCompletion() - DetailContext(aString(), completion, completion, false, Random.nextBoolean(), aString(), CodewhispererCompletionType.Line) - } - - details.add(toAdd) - } - - val recommendationContext = RecommendationContext( - details, - aString(), - aString(), - VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)) - ) - - val selectedIndex = decisions.indexOfFirst { it == CodewhispererSuggestionState.Accept }.let { - if (it != -1) { - it - } else { - 0 - } - } - - val seen = mutableSetOf() - decisions.forEachIndexed { index, decision -> - if (decision != CodewhispererSuggestionState.Unseen) { - seen.add(index) - } - } - - val sessionContext = SessionContext( - selectedIndex = selectedIndex, - seen = seen - ) - return recommendationContext to sessionContext -} - -fun aCompletion(content: String? = null, isEmpty: Boolean = false, referenceCount: Int? = null, importCount: Int? = null): Completion { - val myReferenceCount = referenceCount ?: Random.nextInt(0, 4) - val myImportCount = importCount ?: Random.nextInt(0, 4) - - val references = List(myReferenceCount) { - Reference.builder() - .licenseName(aString()) - .build() - } - - val imports = List(myImportCount) { - Import.builder() - .statement(aString()) - .build() - } - - return Completion.builder() - .content(content ?: if (!isEmpty) aString() else "") - .references(references) - .mostRelevantMissingImports(imports) - .build() -} - -fun aResponseContext(): ResponseContext = ResponseContext(aString()) - -fun aFileContextInfo(language: CodeWhispererProgrammingLanguage? = null): FileContextInfo { - val caretContextInfo = CaretContext(aString(), aString(), aString()) - val fileName = aString() - val fileRelativePath = Paths.get("test", fileName).toString() - val fileUri = "file:///$fileRelativePath" - val programmingLanguage = language ?: listOf( - CodeWhispererPython.INSTANCE, - CodeWhispererJava.INSTANCE - ).random() - - return FileContextInfo(caretContextInfo, fileName, programmingLanguage, fileRelativePath, fileUri) -} - -fun aTriggerType(): CodewhispererTriggerType = - CodewhispererTriggerType.values().filterNot { it == CodewhispererTriggerType.Unknown }.random() - -fun aCompletionType(): CodewhispererCompletionType = - CodewhispererCompletionType.values().filterNot { it == CodewhispererCompletionType.Unknown }.random() - -fun aSuggestionState(): CodewhispererSuggestionState = - CodewhispererSuggestionState.values().filterNot { it == CodewhispererSuggestionState.Unknown }.random() - -fun aProgrammingLanguage(): CodeWhispererProgrammingLanguage = listOf( - CodeWhispererJava.INSTANCE, - CodeWhispererPython.INSTANCE, - CodeWhispererJavaScript.INSTANCE, - CodeWhispererTypeScript.INSTANCE, - CodeWhispererJsx.INSTANCE, - CodeWhispererCsharp.INSTANCE, - CodeWhispererKotlin.INSTANCE, - CodeWhispererC.INSTANCE, - CodeWhispererCpp.INSTANCE, - CodeWhispererGo.INSTANCE, - CodeWhispererPhp.INSTANCE, - CodeWhispererRuby.INSTANCE, - CodeWhispererScala.INSTANCE, - CodeWhispererShell.INSTANCE, - CodeWhispererSql.INSTANCE -).random() diff --git a/plugins/amazonq/codewhisperer/jetbrains-ultimate/tst/software/aws/toolkits/jetbrains/services/codewhisperer/NodeJsCodeWhispererFileContextProviderTest.kt b/plugins/amazonq/codewhisperer/jetbrains-ultimate/tst/software/aws/toolkits/jetbrains/services/codewhisperer/NodeJsCodeWhispererFileContextProviderTest.kt deleted file mode 100644 index f9296cace73..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-ultimate/tst/software/aws/toolkits/jetbrains/services/codewhisperer/NodeJsCodeWhispererFileContextProviderTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 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.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.testFramework.DisposableRule -import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript -import software.aws.toolkits.jetbrains.services.codewhisperer.util.DefaultCodeWhispererFileContextProvider -import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider -import software.aws.toolkits.jetbrains.utils.rules.HeavyJavaCodeInsightTestFixtureRule -import software.aws.toolkits.jetbrains.utils.satisfiesKt - -class NodeJsCodeWhispererFileContextProviderTest { - @JvmField - @Rule - val projectRule = HeavyJavaCodeInsightTestFixtureRule() - - @JvmField - @Rule - val disposableRule = DisposableRule() - - lateinit var sut: DefaultCodeWhispererFileContextProvider - - lateinit var fixture: JavaCodeInsightTestFixture - lateinit var project: Project - - @Before - fun setup() { - fixture = projectRule.fixture - project = projectRule.project - - sut = FileContextProvider.getInstance(project) as DefaultCodeWhispererFileContextProvider - } - - @Test - fun `extractSupplementalFileContext on background should not cause read lock error`() = runTest { - // regression test for https://github.com/aws/aws-toolkit-jetbrains/issues/4888 - assertThat(ApplicationManager.getApplication()).satisfiesKt { - assertThat(it.isDispatchThread).isFalse() - assertThat(it.isReadAccessAllowed).isFalse() - } - - val psiFile = fixture.configureByText("test.d.ts", "") - - val fileContext = aFileContextInfo(CodeWhispererTypeScript.INSTANCE) - sut.extractSupplementalFileContext(psiFile, fileContext, 50) - } -} diff --git a/plugins/amazonq/mynah-ui/package-lock.json b/plugins/amazonq/mynah-ui/package-lock.json index 1849ee5a11f..a16f304d31b 100644 --- a/plugins/amazonq/mynah-ui/package-lock.json +++ b/plugins/amazonq/mynah-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@aws/mynah-ui-chat": "npm:@aws/mynah-ui@4.22.1", + "@aws/mynah-ui-chat": "npm:@aws/mynah-ui@4.30.3", "@types/node": "^14.18.5", "fs-extra": "^10.0.1", "sanitize-html": "^2.12.1", @@ -18,6 +18,7 @@ "web-tree-sitter": "^0.20.7" }, "devDependencies": { + "@aws/chat-client": "^0.1.4", "@aws/fully-qualified-names": "^2.1.1", "@types/sanitize-html": "^2.8.0", "@typescript-eslint/eslint-plugin": "^5.38.0", @@ -46,6 +47,28 @@ "node": ">=0.10.0" } }, + "node_modules/@aws/chat-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@aws/chat-client/-/chat-client-0.1.4.tgz", + "integrity": "sha512-5iqo9f/FjipyWxVPByVcI4yF9NPDOFInuS2ak4bK+j4d6ca1n20CnQrEQcMOdGjl5mde51s7X4Jqvlu3smgHGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws/chat-client-ui-types": "^0.1.12", + "@aws/language-server-runtimes-types": "^0.1.10", + "@aws/mynah-ui": "^4.28.0" + } + }, + "node_modules/@aws/chat-client-ui-types": { + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.32.tgz", + "integrity": "sha512-axJymxFQhXh8AOnhek61VL5va917TjIvfHF5sHpyQay+znILAs65osdIMeqHs6VTjZCKtzIEp5iCd6uZbvYRVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws/language-server-runtimes-types": "^0.1.26" + } + }, "node_modules/@aws/fully-qualified-names": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@aws/fully-qualified-names/-/fully-qualified-names-2.1.2.tgz", @@ -55,11 +78,46 @@ "web-tree-sitter": "^0.20.7" } }, + "node_modules/@aws/language-server-runtimes-types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.26.tgz", + "integrity": "sha512-c63rpUbcrtLqaC33t6elRApQqLbQvFgKzIQ2z/VCavE5F7HSLBfzhHkhgUFd775fBpsF4MHrIzwNitYLhDGobw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5" + } + }, + "node_modules/@aws/mynah-ui": { + "version": "4.30.3", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.30.3.tgz", + "integrity": "sha512-Xy22dzCaFUqpdSHMpLa8Dsq98DiAUq49dm7Iu8Yj2YZXSCyfKQiYMJOfwU8IoqeNcEney5JRMJpf+/RysWugbA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache License 2.0", + "dependencies": { + "escape-html": "^1.0.3", + "highlight.js": "^11.11.0", + "just-clone": "^6.2.0", + "marked": "^14.1.0", + "sanitize-html": "^2.12.1", + "unescape-html": "^1.1.0" + }, + "peerDependencies": { + "escape-html": "^1.0.3", + "highlight.js": "^11.11.0", + "just-clone": "^6.2.0", + "marked": "^14.1.0", + "sanitize-html": "^2.12.1", + "unescape-html": "^1.1.0" + } + }, "node_modules/@aws/mynah-ui-chat": { "name": "@aws/mynah-ui", - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.22.1.tgz", - "integrity": "sha512-6mWD5Fp4VDVSKIv3sRKopoeh3GeiXEp2gWXmUWSVE9ccnnnavPyKSebV6vJiHJHtuS1da7i6ZLVednpsV9I49Q==", + "version": "4.30.3", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.30.3.tgz", + "integrity": "sha512-Xy22dzCaFUqpdSHMpLa8Dsq98DiAUq49dm7Iu8Yj2YZXSCyfKQiYMJOfwU8IoqeNcEney5JRMJpf+/RysWugbA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { @@ -3515,6 +3573,20 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", diff --git a/plugins/amazonq/mynah-ui/package.json b/plugins/amazonq/mynah-ui/package.json index 591190cfc30..953b82bc434 100644 --- a/plugins/amazonq/mynah-ui/package.json +++ b/plugins/amazonq/mynah-ui/package.json @@ -12,7 +12,7 @@ "lintfix": "eslint -c .eslintrc.js --fix --ext .ts ." }, "dependencies": { - "@aws/mynah-ui-chat": "npm:@aws/mynah-ui@4.22.1", + "@aws/mynah-ui-chat": "npm:@aws/mynah-ui@4.30.3", "@types/node": "^14.18.5", "fs-extra": "^10.0.1", "sanitize-html": "^2.12.1", @@ -22,6 +22,7 @@ }, "devDependencies": { "@aws/fully-qualified-names": "^2.1.1", + "@aws/chat-client": "^0.1.4", "@types/sanitize-html": "^2.8.0", "@typescript-eslint/eslint-plugin": "^5.38.0", "@typescript-eslint/parser": "^5.38.0", diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/connectorAdapter.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/connectorAdapter.ts new file mode 100644 index 00000000000..0015e5a940c --- /dev/null +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/connectorAdapter.ts @@ -0,0 +1,120 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import {ChatPrompt, MynahUI, QuickActionCommand, QuickActionCommandGroup} from '@aws/mynah-ui-chat' +import { isTabType } from './ui/storages/tabsStorage' +import { WebviewUIHandler } from './ui/main' +import { TabDataGenerator } from './ui/tabs/generator' +import { ChatClientAdapter, ChatEventHandler } from '@aws/chat-client' +import { FqnExtractor } from "./fqn/extractor"; + +export * from "./ui/main"; + +declare global { + interface Window { fqnExtractor: FqnExtractor; } +} + +window.fqnExtractor = new FqnExtractor(); + +export const initiateAdapter = (showWelcomePage: boolean, + disclaimerAcknowledged: boolean, + isFeatureDevEnabled: boolean, + isCodeTransformEnabled: boolean, + isDocEnabled: boolean, + isCodeScanEnabled: boolean, + isCodeTestEnabled: boolean, + ideApiPostMessage: (message: any) => void, + profileName?: string) : HybridChatAdapter => { + return new HybridChatAdapter(showWelcomePage, disclaimerAcknowledged, isFeatureDevEnabled, isCodeTransformEnabled, isDocEnabled, isCodeScanEnabled, isCodeTestEnabled, ideApiPostMessage, profileName) +} + + +// Ref: https://github.com/aws/aws-toolkit-vscode/blob/e9ea8082ffe0b9968a873437407d0b6b31b9e1a5/packages/core/src/amazonq/webview/ui/connectorAdapter.ts#L14 +export class HybridChatAdapter implements ChatClientAdapter { + private uiHandler?: WebviewUIHandler + + private mynahUIRef?: { mynahUI: MynahUI} + + constructor( + + private showWelcomePage: boolean, + private disclaimerAcknowledged: boolean, + private isFeatureDevEnabled: boolean, + private isCodeTransformEnabled: boolean, + private isDocEnabled: boolean, + private isCodeScanEnabled: boolean, + private isCodeTestEnabled: boolean, + private ideApiPostMessage: (message: any) => void, + private profileName?: string, + + ) {} + + /** + * First we create the ui handler to get the props, then once mynah UI gets created flare will re-inject the + * mynah UI instance on the hybrid chat adapter + */ + createChatEventHandler(mynahUIRef: { mynahUI: MynahUI }): ChatEventHandler { + this.mynahUIRef = mynahUIRef + + this.uiHandler = new WebviewUIHandler({ + postMessage: this.ideApiPostMessage, + mynahUIRef: this.mynahUIRef, + showWelcomePage: this.showWelcomePage, + disclaimerAcknowledged: this.disclaimerAcknowledged, + isFeatureDevEnabled: this.isFeatureDevEnabled, + isCodeTransformEnabled: this.isCodeTransformEnabled, + isDocEnabled: this.isDocEnabled, + isCodeScanEnabled: this.isCodeScanEnabled, + isCodeTestEnabled: this.isCodeTestEnabled, + profileName: this.profileName, + hybridChat: true, + }) + + return this.uiHandler.mynahUIProps + } + + isSupportedTab(tabId: string): boolean { + const tabType = this.uiHandler?.tabsStorage.getTab(tabId)?.type + if (!tabType) { + return false + } + return isTabType(tabType) && tabType !== 'cwc' + } + + async handleMessageReceive(message: MessageEvent): Promise { + if (this.uiHandler) { + return this.uiHandler?.connector?.handleMessageReceive(message) + } + + console.error('unknown message: ', message.data) + } + + isSupportedQuickAction(command: string): boolean { + return ( + command === '/dev' || + command === '/test' || + command === '/review' || + command === '/doc' || + command === '/transform' + ) + } + + handleQuickAction(prompt: ChatPrompt, tabId: string, eventId: string | undefined): void { + return this.uiHandler?.quickActionHandler?.handleCommand(prompt, tabId, eventId) + } + + get initialQuickActions(): QuickActionCommandGroup[] { + const tabDataGenerator = new TabDataGenerator({ + isFeatureDevEnabled: this.isFeatureDevEnabled, + isCodeTransformEnabled: this.isCodeTransformEnabled, + isDocEnabled: this.isDocEnabled, + isCodeScanEnabled: this.isCodeScanEnabled, + isCodeTestEnabled: this.isCodeTestEnabled, + profileName: this.profileName + }) + return tabDataGenerator.quickActionsGenerator.generateForTab('cwc') ?? [] + } +} + + diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/extractor.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/extractor.ts index c467d0acf97..2c598c087c1 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/extractor.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/extractor.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/java-import-reader.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/java-import-reader.ts index a9bf1771afe..8209a444db3 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/java-import-reader.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/fqn/java-import-reader.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts index 6ceb88108c4..4efe2623646 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts @@ -109,7 +109,7 @@ export class Connector { private readonly tabsStorage private readonly amazonqCommonsConnector: AmazonQCommonsConnector - private isUIReady = false + isUIReady = false constructor(props: ConnectorProps) { this.sendMessageToExtension = props.sendMessageToExtension diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts index 55e28eaacc5..c5090c15998 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts @@ -8,20 +8,21 @@ import { Connector } from '../connector' import { TabType, TabsStorage } from '../storages/tabsStorage' import { WelcomeFollowupType } from '../apps/amazonqCommonsConnector' import { AuthFollowUpType } from './generator' +import {MynahUIRef} from "../main"; export interface FollowUpInteractionHandlerProps { - mynahUI: MynahUI + mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage } export class FollowUpInteractionHandler { - private mynahUI: MynahUI + private mynahUIRef: MynahUIRef private connector: Connector private tabsStorage: TabsStorage constructor(props: FollowUpInteractionHandlerProps) { - this.mynahUI = props.mynahUI + this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage } @@ -50,16 +51,16 @@ export class FollowUpInteractionHandler { // which will cause an api call // then we can set the loading state to true if (followUp.prompt !== undefined) { - this.mynahUI.updateStore(tabID, { + this.mynahUI?.updateStore(tabID, { loadingChat: true, cancelButtonWhenLoading: false, promptInputDisabledState: true, }) - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.PROMPT, body: followUp.prompt, }) - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.ANSWER_STREAM, body: '', }) @@ -79,7 +80,7 @@ export class FollowUpInteractionHandler { public onWelcomeFollowUpClicked(tabID: string, welcomeFollowUpType: WelcomeFollowupType) { if (welcomeFollowUpType === 'continue-to-chat') { - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.ANSWER, body: 'Ok, please write your question below.', }) @@ -107,15 +108,15 @@ export class FollowUpInteractionHandler { // which will cause an api call // then we can set the loading state to true if (followUp.prompt !== undefined) { - this.mynahUI.updateStore(tabID, { + this.mynahUI?.updateStore(tabID, { loadingChat: true, promptInputDisabledState: true, }) - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.PROMPT, body: followUp.prompt, }) - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.ANSWER_STREAM, body: '', }) @@ -131,4 +132,8 @@ export class FollowUpInteractionHandler { } this.connector.onFollowUpClicked(tabID, messageId, followUp) } + + private get mynahUI(): MynahUI | undefined { + return this.mynahUIRef.mynahUI + } } 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 1d033f1cc65..a44c8d385e4 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts @@ -4,14 +4,15 @@ */ import { Connector, CWCChatItem } from './connector' import { - ChatItem, + ChatItem, ChatItemAction, ChatItemType, MynahIcons, MynahUI, - MynahUIDataModel, + MynahUIDataModel, MynahUIProps, NotificationType, ProgressField, QuickActionCommand, - ReferenceTrackerInformation + ReferenceTrackerInformation, + ChatPrompt, } from '@aws/mynah-ui-chat' import './styles/dark.scss' import { TabsStorage, TabType } from './storages/tabsStorage' @@ -26,643 +27,711 @@ import { MessageController } from './messages/controller' import { getActions, getDetails } from './diffTree/actions' import { DiffTreeFileInfo } from './diffTree/types' import './styles.css' -import { ChatPrompt, CodeSelectionType} from "@aws/mynah-ui-chat/dist/static"; import {welcomeScreenTabData} from "./walkthrough/welcome"; import { agentWalkthroughDataModel } from './walkthrough/agent' import {createClickTelemetry, createOpenAgentTelemetry} from "./telemetry/actions"; import {disclaimerAcknowledgeButtonId, disclaimerCard} from "./texts/disclaimer"; + +// Ref: https://github.com/aws/aws-toolkit-vscode/blob/e9ea8082ffe0b9968a873437407d0b6b31b9e1a5/packages/core/src/amazonq/webview/ui/main.ts export const createMynahUI = ( ideApi: any, showWelcomePage: boolean, disclaimerAcknowledged: boolean, - featureDevInitEnabled: boolean, - codeTransformInitEnabled: boolean, - docInitEnabled: boolean, - codeScanEnabled: boolean, - codeTestEnabled: boolean, + isFeatureDevEnabled: boolean, + isCodeTransformEnabled: boolean, + isDocEnabled: boolean, + isCodeScanEnabled: boolean, + isCodeTestEnabled: boolean, highlightCommand?: QuickActionCommand, - profileName?: string -) => { - let disclaimerCardActive = !disclaimerAcknowledged - - // eslint-disable-next-line prefer-const - let mynahUI: MynahUI - // eslint-disable-next-line prefer-const - let connector: Connector - const responseMetadata = new Map() - - const tabsStorage = new TabsStorage({ - onTabTimeout: tabID => { - mynahUI.addChatItem(tabID, { - type: ChatItemType.ANSWER, - body: 'This conversation has timed out after 48 hours. It will not be saved. Start a new conversation.', - }) - mynahUI.updateStore(tabID, { - promptInputDisabledState: true, - promptInputPlaceholder: 'Session ended.', - }) - }, - }) - // Adding the first tab as CWC tab - tabsStorage.addTab({ - id: 'tab-1', - status: 'free', - type: showWelcomePage ? 'welcome' : 'cwc', - isSelected: true, - }) - - // used to keep track of whether featureDev is enabled and has an active idC - let isFeatureDevEnabled = featureDevInitEnabled - - let isCodeTransformEnabled = codeTransformInitEnabled - - let isDocEnabled = docInitEnabled + profileName?: string, - let isCodeScanEnabled = codeScanEnabled - - let isCodeTestEnabled = codeTestEnabled - - const tabDataGenerator = new TabDataGenerator({ +) => { + const handler = new WebviewUIHandler({ + postMessage: ideApi.postMessage, + mynahUIRef: { mynahUI: undefined }, + showWelcomePage, + disclaimerAcknowledged, isFeatureDevEnabled, isCodeTransformEnabled, isDocEnabled, isCodeScanEnabled, isCodeTestEnabled, highlightCommand, - profileName + profileName, + hybridChat: false, }) - // eslint-disable-next-line prefer-const - let followUpsInteractionHandler: FollowUpInteractionHandler - // eslint-disable-next-line prefer-const - let quickActionHandler: QuickActionHandler - // eslint-disable-next-line prefer-const - let textMessageHandler: TextMessageHandler - // eslint-disable-next-line prefer-const - let messageController: MessageController - - // eslint-disable-next-line prefer-const - connector = new Connector({ - tabsStorage, - /** - * Proxy for allowing underlying common connectors to call quick action handlers - */ - handleCommand: (chatPrompt: ChatPrompt, tabId: string) => { - quickActionHandler.handleCommand(chatPrompt, tabId) - }, - onUpdateAuthentication: ( - featureDevEnabled: boolean, - codeTransformEnabled: boolean, - docEnabled: boolean, - codeScanEnabled: boolean, - codeTestEnabled: boolean, - authenticatingTabIDs: string[] - ): void => { - isFeatureDevEnabled = featureDevEnabled - isCodeTransformEnabled = codeTransformEnabled - isDocEnabled = docEnabled - isCodeScanEnabled = codeScanEnabled - isCodeTestEnabled = codeTestEnabled - - quickActionHandler.isFeatureDevEnabled = isFeatureDevEnabled - quickActionHandler.isCodeTransformEnabled = isCodeTransformEnabled - quickActionHandler.isDocEnabled = isDocEnabled - quickActionHandler.isCodeTestEnabled = isCodeTestEnabled - quickActionHandler.isCodeScanEnabled = isCodeScanEnabled - tabDataGenerator.quickActionsGenerator.isFeatureDevEnabled = isFeatureDevEnabled - tabDataGenerator.quickActionsGenerator.isCodeTransformEnabled = isCodeTransformEnabled - tabDataGenerator.quickActionsGenerator.isDocEnabled = isDocEnabled - tabDataGenerator.quickActionsGenerator.isCodeScanEnabled = isCodeScanEnabled - tabDataGenerator.quickActionsGenerator.isCodeTestEnabled = isCodeTestEnabled - - // Set the new defaults for the quick action commands in all tabs now that isFeatureDevEnabled and isCodeTransformEnabled were enabled/disabled - for (const tab of tabsStorage.getTabs()) { - mynahUI.updateStore(tab.id, { - quickActionCommands: tabDataGenerator.quickActionsGenerator.generateForTab(tab.type), + return { + mynahUI: handler.mynahUI, + messageReceiver: handler.connector?.handleMessageReceive, + } +} + +export class WebviewUIHandler { + postMessage: any + showWelcomePage: boolean + disclaimerAcknowledged: boolean + isFeatureDevEnabled: boolean + isCodeTransformEnabled: boolean + isDocEnabled: boolean + isCodeScanEnabled: boolean + isCodeTestEnabled: boolean + highlightCommand?: QuickActionCommand + profileName?: string + responseMetadata: Map + tabsStorage: TabsStorage + + mynahUIProps: MynahUIProps + connector?: Connector + tabDataGenerator?: TabDataGenerator + followUpsInteractionHandler?: FollowUpInteractionHandler + quickActionHandler?: QuickActionHandler + textMessageHandler?: TextMessageHandler + messageController?: MessageController + + savedContextCommands: MynahUIDataModel['contextCommands'] + disclaimerCardActive : boolean + + + mynahUIRef: { mynahUI: MynahUI | undefined } + constructor({ + postMessage, + mynahUIRef, + showWelcomePage, + disclaimerAcknowledged, + isFeatureDevEnabled, + isCodeTransformEnabled, + isDocEnabled, + isCodeScanEnabled, + isCodeTestEnabled, + highlightCommand, + profileName, + hybridChat, + + } : { + postMessage: any + mynahUIRef: { mynahUI: MynahUI | undefined } + showWelcomePage: boolean, + disclaimerAcknowledged: boolean, + isFeatureDevEnabled: boolean + isCodeTransformEnabled: boolean + isDocEnabled: boolean + isCodeScanEnabled: boolean + isCodeTestEnabled: boolean + highlightCommand?: QuickActionCommand, + profileName?: string, + hybridChat?: boolean + + + }) { + this.postMessage = postMessage + this.mynahUIRef = mynahUIRef + this.showWelcomePage = showWelcomePage; + this.disclaimerAcknowledged = disclaimerAcknowledged + this.isFeatureDevEnabled = isFeatureDevEnabled + this.isCodeTransformEnabled = isCodeTransformEnabled + this.isDocEnabled = isDocEnabled + this.isCodeScanEnabled = isCodeScanEnabled + this.isCodeTestEnabled = isCodeTestEnabled + this.profileName = profileName + this.responseMetadata = new Map() + this.disclaimerCardActive = !disclaimerAcknowledged + + + this.tabsStorage = new TabsStorage({ + onTabTimeout: tabID => { + this.mynahUI?.addChatItem(tabID, { + type: ChatItemType.ANSWER, + body: 'This conversation has timed out after 48 hours. It will not be saved. Start a new conversation.', + }) + this.mynahUI?.updateStore(tabID, { + promptInputDisabledState: true, + promptInputPlaceholder: 'Session ended.', + }) + }, + }) + + this.tabDataGenerator = new TabDataGenerator({ + isFeatureDevEnabled, + isCodeTransformEnabled, + isDocEnabled, + isCodeScanEnabled, + isCodeTestEnabled, + highlightCommand, + profileName + }) + + this.connector = new Connector({ + tabsStorage: this.tabsStorage, + /** + * Proxy for allowing underlying common connectors to call quick action handlers + */ + handleCommand: (chatPrompt: ChatPrompt, tabId: string) => { + this.quickActionHandler?.handleCommand(chatPrompt, tabId) + }, + onUpdateAuthentication: ( + featureDevEnabled: boolean, + codeTransformEnabled: boolean, + docEnabled: boolean, + codeScanEnabled: boolean, + codeTestEnabled: boolean, + authenticatingTabIDs: string[] + ): void => { + isFeatureDevEnabled = featureDevEnabled + isCodeTransformEnabled = codeTransformEnabled + isDocEnabled = docEnabled + isCodeScanEnabled = codeScanEnabled + isCodeTestEnabled = codeTestEnabled + + this.quickActionHandler = new QuickActionHandler({ + mynahUIRef: this.mynahUIRef, + connector: this.connector!, + tabsStorage: this.tabsStorage, + isFeatureDevEnabled: this.isFeatureDevEnabled, + isCodeTransformEnabled: this.isCodeTransformEnabled, + isDocEnabled: this.isDocEnabled, + isCodeScanEnabled: this.isCodeScanEnabled, + isCodeTestEnabled: this.isCodeTestEnabled, + hybridChat }) - } - // Unlock every authenticated tab that is now authenticated - for (const tabID of authenticatingTabIDs) { - const tabType = tabsStorage.getTab(tabID)?.type - if ( - (tabType === 'featuredev' && featureDevEnabled) || - (tabType === 'codetransform' && codeTransformEnabled) || - (tabType === 'doc' && docEnabled) || - (tabType === 'codetransform' && codeTransformEnabled) || - (tabType === 'codetest' && codeTestEnabled) - ) { - mynahUI.addChatItem(tabID, { - type: ChatItemType.ANSWER, - body: 'Authentication successful. Connected to Amazon Q.', - }) - mynahUI.updateStore(tabID, { - // Always disable prompt for code transform tabs - promptInputDisabledState: tabType === 'codetransform', + this.tabDataGenerator = new TabDataGenerator({ + isFeatureDevEnabled, + isCodeTransformEnabled, + isDocEnabled, + isCodeScanEnabled, + isCodeTestEnabled, + highlightCommand, + profileName + }) + + // Set the new defaults for the quick action commands in all tabs now that isFeatureDevEnabled and isCodeTransformEnabled were enabled/disabled + for (const tab of this.tabsStorage.getTabs()) { + this.mynahUI?.updateStore(tab.id, { + quickActionCommands: this.tabDataGenerator.quickActionsGenerator.generateForTab(tab.type), }) } - } - }, - onFileActionClick: (): void => {}, - onCWCOnboardingPageInteractionMessage: (message: ChatItem): string | undefined => { - return messageController.sendMessageToTab(message, 'cwc') - }, - onCWCContextCommandMessage: (message: ChatItem, command?: string): string | undefined => { - if (command === 'aws.amazonq.sendToPrompt') { - return messageController.sendSelectedCodeToTab(message) - } else { - const tabID = messageController.sendMessageToTab(message, 'cwc') - if (tabID && command) { - ideApi.postMessage(createOpenAgentTelemetry('cwc', 'right-click')) + + // Unlock every authenticated tab that is now authenticated + for (const tabID of authenticatingTabIDs) { + const tabType = this.tabsStorage.getTab(tabID)?.type + if ( + (tabType === 'featuredev' && featureDevEnabled) || + (tabType === 'codetransform' && codeTransformEnabled) || + (tabType === 'doc' && docEnabled) || + (tabType === 'codetransform' && codeTransformEnabled) || + (tabType === 'codetest' && codeTestEnabled) + ) { + this.mynahUI?.addChatItem(tabID, { + type: ChatItemType.ANSWER, + body: 'Authentication successful. Connected to Amazon Q.', + }) + this.mynahUI?.updateStore(tabID, { + // Always disable prompt for code transform tabs + promptInputDisabledState: tabType === 'codetransform', + }) + } } + }, + onFileActionClick: (): void => {}, + onCWCOnboardingPageInteractionMessage: (message: ChatItem): string | undefined => { + return this.messageController?.sendMessageToTab(message, 'cwc') + }, + onCWCContextCommandMessage: (message: ChatItem, command?: string): string | undefined => { + if (command === 'aws.amazonq.sendToPrompt') { + return this.messageController?.sendSelectedCodeToTab(message) + } else { + const tabID = this.messageController?.sendMessageToTab(message, 'cwc') + if (tabID && command) { + this.postMessage.postMessage(createOpenAgentTelemetry('cwc', 'right-click')) + } - return tabID - } - }, - onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => { - followUpsInteractionHandler.onWelcomeFollowUpClicked(tabID, welcomeFollowUpType) - }, - onChatInputEnabled: (tabID: string, enabled: boolean) => { - mynahUI.updateStore(tabID, { - promptInputDisabledState: tabsStorage.isTabDead(tabID) || !enabled, - }) - }, - onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined, cancelButtonWhenLoading: boolean = false) => { - if (inProgress) { - mynahUI.updateStore(tabID, { - loadingChat: true, - promptInputDisabledState: true, - cancelButtonWhenLoading, + return tabID + } + }, + onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => { + this.followUpsInteractionHandler?.onWelcomeFollowUpClicked(tabID, welcomeFollowUpType) + }, + onChatInputEnabled: (tabID: string, enabled: boolean) => { + this.mynahUI?.updateStore(tabID, { + promptInputDisabledState: this.tabsStorage.isTabDead(tabID) || !enabled, }) - if (message) { - mynahUI.updateLastChatAnswer(tabID, { - body: message, + }, + onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined, cancelButtonWhenLoading: boolean = false) => { + if (inProgress) { + this.mynahUI?.updateStore(tabID, { + loadingChat: true, + promptInputDisabledState: true, + cancelButtonWhenLoading, + }) + if (message) { + this.mynahUI?.updateLastChatAnswer(tabID, { + body: message, + }) + } + this.mynahUI?.addChatItem(tabID, { + type: ChatItemType.ANSWER_STREAM, + body: '', }) + this.tabsStorage.updateTabStatus(tabID, 'busy') + return } - mynahUI.addChatItem(tabID, { - type: ChatItemType.ANSWER_STREAM, - body: '', - }) - tabsStorage.updateTabStatus(tabID, 'busy') - return - } - mynahUI.updateStore(tabID, { - loadingChat: false, - promptInputDisabledState: tabsStorage.isTabDead(tabID), - }) - tabsStorage.updateTabStatus(tabID, 'free') - }, - onCodeTransformChatDisabled: (tabID: string) => { - // Clear the chat window to prevent button clicks or form selections - mynahUI.updateStore(tabID, { - loadingChat: false, - chatItems: [], - }) - }, - onCodeTransformMessageReceived: ( - tabID: string, - chatItem: ChatItem, - isLoading: boolean, - clearPreviousItemButtons?: boolean - ) => { - if (chatItem.type === ChatItemType.ANSWER_PART) { - mynahUI.updateLastChatAnswer(tabID, { - ...(chatItem.messageId !== undefined ? { messageId: chatItem.messageId } : {}), - ...(chatItem.canBeVoted !== undefined ? { canBeVoted: chatItem.canBeVoted } : {}), - ...(chatItem.codeReference !== undefined ? { codeReference: chatItem.codeReference } : {}), - ...(chatItem.body !== undefined ? { body: chatItem.body } : {}), - ...(chatItem.relatedContent !== undefined ? { relatedContent: chatItem.relatedContent } : {}), - ...(chatItem.formItems !== undefined ? { formItems: chatItem.formItems } : {}), - ...(chatItem.buttons !== undefined ? { buttons: chatItem.buttons } : { buttons: [] }), - // For loading animation to work, do not update the chat item type - ...(chatItem.followUp !== undefined ? { followUp: chatItem.followUp } : {}), + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + promptInputDisabledState: this.tabsStorage.isTabDead(tabID), }) - - if (!isLoading) { - mynahUI.updateStore(tabID, { - loadingChat: false, + this.tabsStorage.updateTabStatus(tabID, 'free') + }, + onCodeTransformChatDisabled: (tabID: string) => { + // Clear the chat window to prevent button clicks or form selections + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + chatItems: [], + }) + }, + onCodeTransformMessageReceived: ( + tabID: string, + chatItem: ChatItem, + isLoading: boolean, + clearPreviousItemButtons?: boolean + ) => { + if (chatItem.type === ChatItemType.ANSWER_PART) { + this.mynahUI?.updateLastChatAnswer(tabID, { + ...(chatItem.messageId !== undefined ? { messageId: chatItem.messageId } : {}), + ...(chatItem.canBeVoted !== undefined ? { canBeVoted: chatItem.canBeVoted } : {}), + ...(chatItem.codeReference !== undefined ? { codeReference: chatItem.codeReference } : {}), + ...(chatItem.body !== undefined ? { body: chatItem.body } : {}), + ...(chatItem.relatedContent !== undefined ? { relatedContent: chatItem.relatedContent } : {}), + ...(chatItem.formItems !== undefined ? { formItems: chatItem.formItems } : {}), + ...(chatItem.buttons !== undefined ? { buttons: chatItem.buttons } : { buttons: [] }), + // For loading animation to work, do not update the chat item type + ...(chatItem.followUp !== undefined ? { followUp: chatItem.followUp } : {}), }) + + if (!isLoading) { + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + }) + } + + return } - return - } + if ( + chatItem.type === ChatItemType.PROMPT || + chatItem.type === ChatItemType.ANSWER_STREAM || + chatItem.type === ChatItemType.ANSWER + ) { + if (chatItem.followUp === undefined && clearPreviousItemButtons === true) { + this.mynahUI?.updateLastChatAnswer(tabID, { + buttons: [], + followUp: { options: [] }, + }) + } + + this.mynahUI?.addChatItem(tabID, chatItem) + this.mynahUI?.updateStore(tabID, { + cancelButtonWhenLoading: false, + loadingChat: chatItem.type !== ChatItemType.ANSWER, + }) - if ( - chatItem.type === ChatItemType.PROMPT || - chatItem.type === ChatItemType.ANSWER_STREAM || - chatItem.type === ChatItemType.ANSWER - ) { - if (chatItem.followUp === undefined && clearPreviousItemButtons === true) { - mynahUI.updateLastChatAnswer(tabID, { - buttons: [], - followUp: { options: [] }, + if (chatItem.type === ChatItemType.PROMPT) { + this.tabsStorage.updateTabStatus(tabID, 'busy') + } else if (chatItem.type === ChatItemType.ANSWER) { + this.tabsStorage.updateTabStatus(tabID, 'free') + } + } + }, + onCodeTransformMessageUpdate: (tabID: string, messageId: string, chatItem: Partial) => { + this.mynahUI?.updateChatAnswerWithMessageId(tabID, messageId, chatItem) + }, + onNotification: (notification: { content: string; title?: string; type: NotificationType }) => { + this.mynahUI?.notify(notification) + }, + onCodeTransformCommandMessageReceived: (_message: ChatItem, command?: string) => { + if (command === 'stop') { + const codeTransformTab = this.tabsStorage.getTabs().find(tab => tab.type === 'codetransform') + if (codeTransformTab !== undefined && codeTransformTab.isSelected) { + return + } + + this.mynahUI?.notify({ + type: NotificationType.INFO, + title: 'Q - Transform', + content: `Amazon Q is stopping your transformation. To view progress in the Q - Transform tab, click anywhere on this notification.`, + duration: 10000, + onNotificationClick: eventId => { + if (codeTransformTab !== undefined) { + // Click to switch to the opened code transform tab + this.mynahUI?.selectTab(codeTransformTab.id, eventId) + } else { + // Click to open a new code transform tab + this.quickActionHandler?.handleCommand({ command: '/transform' }, '', eventId) + } + }, }) } + }, + sendMessageToExtension: message => { + postMessage.postMessage(message) + }, + onChatAnswerUpdated: (tabID: string, item) => { + if (item.messageId !== undefined) { + this.mynahUI?.updateChatAnswerWithMessageId(tabID, item.messageId, { + ...(item.body !== undefined ? { body: item.body } : {}), + ...(item.buttons !== undefined ? { buttons: item.buttons } : {}), + ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), + ...(item.footer !== undefined ? { footer: item.footer } : {}), + ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), + ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), + }) + } else { + this.mynahUI?.updateLastChatAnswer(tabID, { + ...(item.body !== undefined ? { body: item.body } : {}), + ...(item.buttons !== undefined ? { buttons: item.buttons } : {}), + ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), + ...(item.footer !== undefined ? { footer: item.footer } : {}), + ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), + ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), + } as ChatItem) + } + }, + onChatAnswerReceived: (tabID: string, item: CWCChatItem) => { + if (item.type === ChatItemType.ANSWER_PART || item.type === ChatItemType.CODE_RESULT) { + this.mynahUI?.updateLastChatAnswer(tabID, { + ...(item.messageId !== undefined ? { messageId: item.messageId } : {}), + ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), + ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), + ...(item.body !== undefined ? { body: item.body } : {}), + ...(item.relatedContent !== undefined ? { relatedContent: item.relatedContent } : {}), + ...(item.type === ChatItemType.CODE_RESULT + ? { type: ChatItemType.CODE_RESULT, fileList: item.fileList } + : {}), + ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), + }) + if (item.messageId !== undefined && item.userIntent !== undefined && item.codeBlockLanguage !== undefined) { + this.responseMetadata.set(item.messageId, [item.userIntent, item.codeBlockLanguage]) + } + return + } - mynahUI.addChatItem(tabID, chatItem) - mynahUI.updateStore(tabID, { - cancelButtonWhenLoading: false, - loadingChat: chatItem.type !== ChatItemType.ANSWER, - }) - - if (chatItem.type === ChatItemType.PROMPT) { - tabsStorage.updateTabStatus(tabID, 'busy') - } else if (chatItem.type === ChatItemType.ANSWER) { - tabsStorage.updateTabStatus(tabID, 'free') + if (item.body !== undefined || item.relatedContent !== undefined || item.followUp !== undefined) { + this.mynahUI?.addChatItem(tabID, item) } - } - }, - onCodeTransformMessageUpdate: (tabID: string, messageId: string, chatItem: Partial) => { - mynahUI.updateChatAnswerWithMessageId(tabID, messageId, chatItem) - }, - onNotification: (notification: { content: string; title?: string; type: NotificationType }) => { - mynahUI.notify(notification) - }, - onCodeTransformCommandMessageReceived: (_message: ChatItem, command?: string) => { - if (command === 'stop') { - const codeTransformTab = tabsStorage.getTabs().find(tab => tab.type === 'codetransform') - if (codeTransformTab !== undefined && codeTransformTab.isSelected) { + + if ( + item.type === ChatItemType.PROMPT || + item.type === ChatItemType.SYSTEM_PROMPT || + item.type === ChatItemType.AI_PROMPT + ) { + this.mynahUI?.updateStore(tabID, { + loadingChat: true, + cancelButtonWhenLoading: false, + promptInputDisabledState: true, + }) + + this.tabsStorage.updateTabStatus(tabID, 'busy') return } - mynahUI.notify({ - type: NotificationType.INFO, - title: 'Q - Transform', - content: `Amazon Q is stopping your transformation. To view progress in the Q - Transform tab, click anywhere on this notification.`, - duration: 10000, - onNotificationClick: eventId => { - if (codeTransformTab !== undefined) { - // Click to switch to the opened code transform tab - mynahUI.selectTab(codeTransformTab.id, eventId) - } else { - // Click to open a new code transform tab - quickActionHandler.handleCommand({ command: '/transform' }, '', eventId) - } + if (item.type === ChatItemType.ANSWER) { + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + promptInputDisabledState: this.tabsStorage.isTabDead(tabID), + }) + this.tabsStorage.updateTabStatus(tabID, 'free') + } + }, + onRunTestMessageReceived: (tabID: string, shouldRunTestMessage: boolean) => { + if (shouldRunTestMessage) { + this.quickActionHandler?.handleCommand({ command: '/test' }, tabID) + } + }, + onMessageReceived: (tabID: string, messageData: MynahUIDataModel) => { + this.mynahUI?.updateStore(tabID, messageData) + }, + onFileComponentUpdate: ( + tabID: string, + filePaths: DiffTreeFileInfo[], + deletedFiles: DiffTreeFileInfo[], + messageId: string, + disableFileActions: boolean = false + ) => { + const updateWith: Partial = { + type: ChatItemType.ANSWER, + fileList: { + rootFolderTitle: 'Changes', + filePaths: filePaths.map(i => i.zipFilePath), + deletedFiles: deletedFiles.map(i => i.zipFilePath), + details: getDetails([...filePaths, ...deletedFiles]), + actions: disableFileActions ? undefined : getActions([...filePaths, ...deletedFiles]), }, + } + this.mynahUI?.updateChatAnswerWithMessageId(tabID, messageId, updateWith) + }, + onWarning: (tabID: string, message: string, title: string) => { + this.mynahUI?.notify({ + title: title, + content: message, + type: NotificationType.WARNING, }) - } - }, - sendMessageToExtension: message => { - ideApi.postMessage(message) - }, - onChatAnswerUpdated: (tabID: string, item) => { - if (item.messageId !== undefined) { - mynahUI.updateChatAnswerWithMessageId(tabID, item.messageId, { - ...(item.body !== undefined ? { body: item.body } : {}), - ...(item.buttons !== undefined ? { buttons: item.buttons } : {}), - ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), - ...(item.footer !== undefined ? { footer: item.footer } : {}), - ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), - ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), - }) - } else { - mynahUI.updateLastChatAnswer(tabID, { - ...(item.body !== undefined ? { body: item.body } : {}), - ...(item.buttons !== undefined ? { buttons: item.buttons } : {}), - ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), - ...(item.footer !== undefined ? { footer: item.footer } : {}), - ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), - ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), - } as ChatItem) - } - }, - onChatAnswerReceived: (tabID: string, item: CWCChatItem) => { - if (item.type === ChatItemType.ANSWER_PART || item.type === ChatItemType.CODE_RESULT) { - mynahUI.updateLastChatAnswer(tabID, { - ...(item.messageId !== undefined ? { messageId: item.messageId } : {}), - ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}), - ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), - ...(item.body !== undefined ? { body: item.body } : {}), - ...(item.relatedContent !== undefined ? { relatedContent: item.relatedContent } : {}), - ...(item.type === ChatItemType.CODE_RESULT - ? { type: ChatItemType.CODE_RESULT, fileList: item.fileList } - : {}), - ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}), + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + promptInputDisabledState: this.tabsStorage.isTabDead(tabID), }) - if (item.messageId !== undefined && item.userIntent !== undefined && item.codeBlockLanguage !== undefined) { - responseMetadata.set(item.messageId, [item.userIntent, item.codeBlockLanguage]) + this.tabsStorage.updateTabStatus(tabID, 'free') + }, + onError: (tabID: string, message: string, title: string) => { + const answer: ChatItem = { + type: ChatItemType.ANSWER, + body: `**${title}**${message}`, } - return - } - if (item.body !== undefined || item.relatedContent !== undefined || item.followUp !== undefined) { - mynahUI.addChatItem(tabID, item) - } + if (tabID !== '') { + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + promptInputDisabledState: this.tabsStorage.isTabDead(tabID), + }) + this.tabsStorage.updateTabStatus(tabID, 'free') - if ( - item.type === ChatItemType.PROMPT || - item.type === ChatItemType.SYSTEM_PROMPT || - item.type === ChatItemType.AI_PROMPT - ) { - mynahUI.updateStore(tabID, { - loadingChat: true, - cancelButtonWhenLoading: false, - promptInputDisabledState: true, + this.mynahUI?.addChatItem(tabID, answer) + } else { + const newTabId = this.mynahUI?.updateStore('', { + tabTitle: 'Error', + quickActionCommands: [], + promptInputPlaceholder: '', + chatItems: [answer], + }) + if (newTabId === undefined) { + this.mynahUI?.notify({ + content: uiComponentsTexts.noMoreTabsTooltip, + type: NotificationType.WARNING, + }) + return + } else { + // TODO remove this since it will be added with the onTabAdd and onTabAdd is now sync, + // It means that it cannot trigger after the updateStore function returns. + this.tabsStorage.addTab({ + id: newTabId, + status: 'busy', + type: 'cwc', + isSelected: true, + }) + } + } + return + }, + onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => { + this.mynahUI?.updateStore(tabID, { + promptInputPlaceholder: newPlaceholder, }) + }, + onUpdatePromptProgress: (tabID: string, progressField: ProgressField | null | undefined) => { + this.mynahUI?.updateStore(tabID, { + // eslint-disable-next-line no-null/no-null + promptInputProgress: progressField ? progressField : null, + }) + }, + onNewTab: (tabType: TabType) => { + const newTabID = this.mynahUI?.updateStore('', {}) + if (!newTabID) { + return + } - tabsStorage.updateTabStatus(tabID, 'busy') - return - } + this.tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) + this.connector?.onKnownTabOpen(newTabID) + this.connector?.onUpdateTabType(newTabID) - if (item.type === ChatItemType.ANSWER) { - mynahUI.updateStore(tabID, { - loadingChat: false, - promptInputDisabledState: tabsStorage.isTabDead(tabID), + this.mynahUI?.updateStore(newTabID, this.tabDataGenerator!.getTabData(tabType, true)) + }, + onStartNewTransform: (tabID: string) => { + this.mynahUI?.updateStore(tabID, { chatItems: [] }) + this.mynahUI?.updateStore(tabID, this.tabDataGenerator!.getTabData('codetransform', true)) + }, + onOpenSettingsMessage: (tabId: string) => { + this.mynahUI?.addChatItem(tabId, { + type: ChatItemType.ANSWER, + body: `You need to enable local workspace index in Amazon Q settings.`, + buttons: [ + { + id: 'open-settings', + text: 'Open settings', + icon: MynahIcons.EXTERNAL, + keepCardAfterClick: false, + status: 'info', + }, + ], }) - tabsStorage.updateTabStatus(tabID, 'free') - } - }, - onRunTestMessageReceived: (tabID: string, shouldRunTestMessage: boolean) => { - if (shouldRunTestMessage) { - quickActionHandler.handleCommand({ command: '/test' }, tabID) - } - }, - onMessageReceived: (tabID: string, messageData: MynahUIDataModel) => { - mynahUI.updateStore(tabID, messageData) - }, - onFileComponentUpdate: ( - tabID: string, - filePaths: DiffTreeFileInfo[], - deletedFiles: DiffTreeFileInfo[], - messageId: string, - disableFileActions: boolean = false - ) => { - const updateWith: Partial = { - type: ChatItemType.ANSWER, - fileList: { - rootFolderTitle: 'Changes', - filePaths: filePaths.map(i => i.zipFilePath), - deletedFiles: deletedFiles.map(i => i.zipFilePath), - details: getDetails([...filePaths, ...deletedFiles]), - actions: disableFileActions ? undefined : getActions([...filePaths, ...deletedFiles]), - }, - } - mynahUI.updateChatAnswerWithMessageId(tabID, messageId, updateWith) - }, - onWarning: (tabID: string, message: string, title: string) => { - mynahUI.notify({ - title: title, - content: message, - type: NotificationType.WARNING, - }) - mynahUI.updateStore(tabID, { - loadingChat: false, - promptInputDisabledState: tabsStorage.isTabDead(tabID), - }) - tabsStorage.updateTabStatus(tabID, 'free') - }, - onError: (tabID: string, message: string, title: string) => { - const answer: ChatItem = { - type: ChatItemType.ANSWER, - body: `**${title}** - ${message}`, - } - - if (tabID !== '') { - mynahUI.updateStore(tabID, { + this.tabsStorage.updateTabStatus(tabId, 'free') + this.mynahUI?.updateStore(tabId, { loadingChat: false, - promptInputDisabledState: tabsStorage.isTabDead(tabID), - }) - tabsStorage.updateTabStatus(tabID, 'free') - - mynahUI.addChatItem(tabID, answer) - } else { - const newTabId = mynahUI.updateStore('', { - tabTitle: 'Error', - quickActionCommands: [], - promptInputPlaceholder: '', - chatItems: [answer], + promptInputDisabledState: this.tabsStorage.isTabDead(tabId), }) - if (newTabId === undefined) { - mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) + return + }, + onCodeScanMessageReceived: (tabID: string, chatItem: ChatItem, isLoading: boolean, clearPreviousItemButtons?: boolean, runReview?: boolean) => { + if (runReview) { + this.quickActionHandler?.handleCommand({ command: "/review" }, "") return - } else { - // TODO remove this since it will be added with the onTabAdd and onTabAdd is now sync, - // It means that it cannot trigger after the updateStore function returns. - tabsStorage.addTab({ - id: newTabId, - status: 'busy', - type: 'cwc', - isSelected: true, - }) } - } - return - }, - onUpdatePlaceholder(tabID: string, newPlaceholder: string) { - mynahUI.updateStore(tabID, { - promptInputPlaceholder: newPlaceholder, - }) - }, - onUpdatePromptProgress(tabID: string, progressField: ProgressField | null | undefined) { - mynahUI.updateStore(tabID, { - // eslint-disable-next-line no-null/no-null - promptInputProgress: progressField ? progressField : null, - }) - }, - onNewTab(tabType: TabType) { - const newTabID = mynahUI.updateStore('', {}) - if (!newTabID) { - return - } + if (chatItem.type === ChatItemType.ANSWER_PART) { + this.mynahUI?.updateLastChatAnswer(tabID, { + ...(chatItem.messageId !== undefined ? { messageId: chatItem.messageId } : {}), + ...(chatItem.canBeVoted !== undefined ? { canBeVoted: chatItem.canBeVoted } : {}), + ...(chatItem.codeReference !== undefined ? { codeReference: chatItem.codeReference } : {}), + ...(chatItem.body !== undefined ? { body: chatItem.body } : {}), + ...(chatItem.relatedContent !== undefined ? { relatedContent: chatItem.relatedContent } : {}), + ...(chatItem.formItems !== undefined ? { formItems: chatItem.formItems } : {}), + ...(chatItem.buttons !== undefined ? { buttons: chatItem.buttons } : { buttons: [] }), + // For loading animation to work, do not update the chat item type + ...(chatItem.followUp !== undefined ? { followUp: chatItem.followUp } : {}), + }) - tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) - connector.onKnownTabOpen(newTabID) - connector.onUpdateTabType(newTabID) - - mynahUI.updateStore(newTabID, tabDataGenerator.getTabData(tabType, true)) - }, - onStartNewTransform(tabID: string) { - mynahUI.updateStore(tabID, { chatItems: [] }) - mynahUI.updateStore(tabID, tabDataGenerator.getTabData('codetransform', true)) - }, - onOpenSettingsMessage(tabId: string) { - mynahUI.addChatItem(tabId, { - type: ChatItemType.ANSWER, - body: `You need to enable local workspace index in Amazon Q settings.`, - buttons: [ - { - id: 'open-settings', - text: 'Open settings', - icon: MynahIcons.EXTERNAL, - keepCardAfterClick: false, - status: 'info', - }, - ], - }) - tabsStorage.updateTabStatus(tabId, 'free') - mynahUI.updateStore(tabId, { - loadingChat: false, - promptInputDisabledState: tabsStorage.isTabDead(tabId), - }) - return - }, - onCodeScanMessageReceived(tabID: string, chatItem: ChatItem, isLoading: boolean, clearPreviousItemButtons?: boolean, runReview?: boolean) { - if (runReview) { - quickActionHandler.handleCommand({ command: "/review" }, "") - return - } - if (chatItem.type === ChatItemType.ANSWER_PART) { - mynahUI.updateLastChatAnswer(tabID, { - ...(chatItem.messageId !== undefined ? { messageId: chatItem.messageId } : {}), - ...(chatItem.canBeVoted !== undefined ? { canBeVoted: chatItem.canBeVoted } : {}), - ...(chatItem.codeReference !== undefined ? { codeReference: chatItem.codeReference } : {}), - ...(chatItem.body !== undefined ? { body: chatItem.body } : {}), - ...(chatItem.relatedContent !== undefined ? { relatedContent: chatItem.relatedContent } : {}), - ...(chatItem.formItems !== undefined ? { formItems: chatItem.formItems } : {}), - ...(chatItem.buttons !== undefined ? { buttons: chatItem.buttons } : { buttons: [] }), - // For loading animation to work, do not update the chat item type - ...(chatItem.followUp !== undefined ? { followUp: chatItem.followUp } : {}), - }) + if (!isLoading) { + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + }) + } else { + this.mynahUI?.updateStore(tabID, { + cancelButtonWhenLoading: false + }) + } + } - if (!isLoading) { - mynahUI.updateStore(tabID, { - loadingChat: false, + if ( + chatItem.type === ChatItemType.PROMPT || + chatItem.type === ChatItemType.ANSWER_STREAM || + chatItem.type === ChatItemType.ANSWER + ) { + if (chatItem.followUp === undefined && clearPreviousItemButtons === true) { + this.mynahUI?.updateLastChatAnswer(tabID, { + buttons: [], + followUp: { options: [] }, + }) + } + + this.mynahUI?.addChatItem(tabID, chatItem) + this.mynahUI?.updateStore(tabID, { + loadingChat: chatItem.type !== ChatItemType.ANSWER }) - } else { - mynahUI.updateStore(tabID, { - cancelButtonWhenLoading: false + + if (chatItem.type === ChatItemType.PROMPT) { + this.tabsStorage.updateTabStatus(tabID, 'busy') + } else if (chatItem.type === ChatItemType.ANSWER) { + this.tabsStorage.updateTabStatus(tabID, 'free') + } + } + }, + onFeatureConfigsAvailable: ( + highlightCommand?: QuickActionCommand + ): void => { + this.tabDataGenerator!.highlightCommand = highlightCommand + + for (const tab of this.tabsStorage.getTabs()) { + this.mynahUI?.updateStore(tab.id, { + contextCommands: this.tabDataGenerator!.getTabData(tab.type, true).contextCommands }) } } - - if ( - chatItem.type === ChatItemType.PROMPT || - chatItem.type === ChatItemType.ANSWER_STREAM || - chatItem.type === ChatItemType.ANSWER - ) { - if (chatItem.followUp === undefined && clearPreviousItemButtons === true) { - mynahUI.updateLastChatAnswer(tabID, { - buttons: [], - followUp: { options: [] }, + }) + this.mynahUIProps = { + onReady: () => { + // the legacy event flow adds events listeners to the window, we want to avoid these in the lsp flow, since those + // are handled by the flare chat-client + if (hybridChat && this.connector) { + this.connector.isUIReady = true + postMessage.postMessage({ + command: 'ui-is-ready', }) + return } - - mynahUI.addChatItem(tabID, chatItem) - mynahUI.updateStore(tabID, { - loadingChat: chatItem.type !== ChatItemType.ANSWER - }) - - if (chatItem.type === ChatItemType.PROMPT) { - tabsStorage.updateTabStatus(tabID, 'busy') - } else if (chatItem.type === ChatItemType.ANSWER) { - tabsStorage.updateTabStatus(tabID, 'free') - } - } - }, - onFeatureConfigsAvailable: ( - highlightCommand?: QuickActionCommand - ): void => { - tabDataGenerator.highlightCommand = highlightCommand - - for (const tab of tabsStorage.getTabs()) { - mynahUI.updateStore(tab.id, { - contextCommands: tabDataGenerator.getTabData(tab.type, true).contextCommands - }) - } - } - }) - - mynahUI = new MynahUI({ - onReady: connector.uiReady, - onTabAdd: (tabID: string) => { - // If featureDev or gumby has changed availability in between the default store settings and now - // make sure to show/hide it accordingly - mynahUI.updateStore(tabID, { - quickActionCommands: tabDataGenerator.quickActionsGenerator.generateForTab('unknown'), - ...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), - }) - connector.onTabAdd(tabID) - }, - onStopChatResponse: (tabID: string) => { - mynahUI.updateStore(tabID, { - loadingChat: false, - promptInputDisabledState: false, - }) - connector.onStopChatResponse(tabID) - }, - onTabRemove: connector.onTabRemove, - onTabChange: connector.onTabChange, - onChatPrompt: (tabID, prompt, eventId) => { - if ((prompt.prompt ?? '') === '' && (prompt.command ?? '') === '') { - return - } - - if (tabsStorage.getTab(tabID)?.type === 'featuredev') { - mynahUI.addChatItem(tabID, { - type: ChatItemType.ANSWER_STREAM, + this.connector?.uiReady() + }, + onTabAdd: (tabID: string) => { + // If featureDev or gumby has changed availability in between the default store settings and now + // make sure to show/hide it accordingly + this.mynahUI?.updateStore(tabID, { + quickActionCommands: this.tabDataGenerator?.quickActionsGenerator.generateForTab('unknown'), + ...(this.disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), }) - } else if (tabsStorage.getTab(tabID)?.type === 'codetransform') { - connector.requestAnswer(tabID, { - chatMessage: prompt.prompt ?? '' + this.connector?.onTabAdd(tabID) + }, + onStopChatResponse: (tabID: string) => { + this.mynahUI?.updateStore(tabID, { + loadingChat: false, + promptInputDisabledState: false, }) - return - } else if (tabsStorage.getTab(tabID)?.type === 'codetest') { - if(prompt.command !== undefined && prompt.command.trim() !== '' && prompt.command !== '/test') { - quickActionHandler.handleCommand(prompt, tabID, eventId) + this.connector?.onStopChatResponse(tabID) + }, + onTabRemove: this.connector.onTabRemove, + onTabChange: this.connector.onTabChange, + onChatPrompt: (tabID: string, prompt : ChatPrompt, eventId: string | undefined) => { + if ((prompt.prompt ?? '') === '' && (prompt.command ?? '') === '') { return - } else { - connector.requestAnswer(tabID, { + } + + if (this.tabsStorage.getTab(tabID)?.type === 'featuredev') { + this.mynahUI?.addChatItem(tabID, { + type: ChatItemType.ANSWER_STREAM, + }) + } else if (this.tabsStorage.getTab(tabID)?.type === 'codetransform') { + this.connector?.requestAnswer(tabID, { chatMessage: prompt.prompt ?? '' }) return + } else if (this.tabsStorage.getTab(tabID)?.type === 'codetest') { + if(prompt.command !== undefined && prompt.command.trim() !== '' && prompt.command !== '/test') { + this.quickActionHandler?.handleCommand(prompt, tabID, eventId) + return + } else { + this.connector?.requestAnswer(tabID, { + chatMessage: prompt.prompt ?? '' + }) + return + } + } else if (this.tabsStorage.getTab(tabID)?.type === 'codescan') { + if(prompt.command !== undefined && prompt.command.trim() !== '') { + this.quickActionHandler?.handleCommand(prompt, tabID, eventId) + return + } } - } else if (tabsStorage.getTab(tabID)?.type === 'codescan') { - if(prompt.command !== undefined && prompt.command.trim() !== '') { - quickActionHandler.handleCommand(prompt, tabID, eventId) - return - } - } - if (tabsStorage.getTab(tabID)?.type === 'welcome') { - mynahUI.updateStore(tabID, { - tabHeaderDetails: void 0, - compactMode: false, - tabBackground: false, - promptInputText: '', - promptInputLabel: void 0, - chatItems: [], - }) - } + if (this.tabsStorage.getTab(tabID)?.type === 'welcome') { + this.mynahUI?.updateStore(tabID, { + tabHeaderDetails: void 0, + compactMode: false, + tabBackground: false, + promptInputText: '', + promptInputLabel: void 0, + chatItems: [], + }) + } - if (prompt.command !== undefined && prompt.command.trim() !== '') { - quickActionHandler.handleCommand(prompt, tabID, eventId) + if (prompt.command !== undefined && prompt.command.trim() !== '') { + this.quickActionHandler?.handleCommand(prompt, tabID, eventId) - const newTabType = tabsStorage.getSelectedTab()?.type - if (newTabType) { - ideApi.postMessage(createOpenAgentTelemetry(newTabType, 'quick-action')) + const newTabType = this.tabsStorage.getSelectedTab()?.type + if (newTabType) { + postMessage.postMessage(createOpenAgentTelemetry(newTabType, 'quick-action')) + } + return } - return - } - textMessageHandler.handle(prompt, tabID) - }, - onVote: connector.onChatItemVoted, - onSendFeedback: (tabId, feedbackPayload) => { - connector.sendFeedback(tabId, feedbackPayload) - mynahUI.notify({ - type: NotificationType.INFO, - title: 'Your feedback is sent', - content: 'Thanks for your feedback.', - }) - }, - onCodeInsertToCursorPosition: connector.onCodeInsertToCursorPosition, - onCopyCodeToClipboard: ( - tabId, - messageId, - code, - type, - referenceTrackerInfo, - eventId, - codeBlockIndex, - totalCodeBlocks - ) => { - connector.onCopyCodeToClipboard( + this.textMessageHandler!.handle(prompt, tabID) + }, + onVote: this.connector.onChatItemVoted, + onSendFeedback: (tabId, feedbackPayload) => { + this.connector?.sendFeedback(tabId, feedbackPayload) + this.mynahUI?.notify({ + type: NotificationType.INFO, + title: 'Your feedback is sent', + content: 'Thanks for your feedback.', + }) + }, + onCodeInsertToCursorPosition: this.connector.onCodeInsertToCursorPosition, + onCopyCodeToClipboard: ( tabId, messageId, code, @@ -671,147 +740,173 @@ export const createMynahUI = ( eventId, codeBlockIndex, totalCodeBlocks - ) - mynahUI.notify({ - type: NotificationType.SUCCESS, - content: 'Selected code is copied to clipboard', - }) - }, - onChatItemEngagement: connector.triggerSuggestionEngagement, - onSourceLinkClick: (tabId, messageId, link, mouseEvent) => { - mouseEvent?.preventDefault() - mouseEvent?.stopPropagation() - mouseEvent?.stopImmediatePropagation() - connector.onSourceLinkClick(tabId, messageId, link) - }, - onLinkClick: (tabId, messageId, link, mouseEvent) => { - mouseEvent?.preventDefault() - mouseEvent?.stopPropagation() - mouseEvent?.stopImmediatePropagation() - connector.onResponseBodyLinkClick(tabId, messageId, link) - }, - onInfoLinkClick: (tabId: string, link: string, mouseEvent?: MouseEvent) => { - mouseEvent?.preventDefault() - mouseEvent?.stopPropagation() - mouseEvent?.stopImmediatePropagation() - connector.onInfoLinkClick(tabId, link) - }, - onResetStore: () => {}, - onFollowUpClicked: (tabID, messageId, followUp) => { - followUpsInteractionHandler.onFollowUpClicked(tabID, messageId, followUp) - }, - onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => { - connector.onFileActionClick(tabID, messageId, filePath, actionName) - }, - onFileClick: connector.onFileClick, - onChatPromptProgressActionButtonClicked: (tabID, action) => { - connector.onCustomFormAction(tabID, undefined, action) - }, - tabs: { - 'tab-1': { - isSelected: true, - store: { - ...(showWelcomePage - ? welcomeScreenTabData(tabDataGenerator).store - : tabDataGenerator.getTabData('cwc', true)), - ...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), + ) => { + this.connector?.onCopyCodeToClipboard( + tabId, + messageId, + code, + type, + referenceTrackerInfo, + eventId, + codeBlockIndex, + totalCodeBlocks + ) + this.mynahUI?.notify({ + type: NotificationType.SUCCESS, + content: 'Selected code is copied to clipboard', + }) + }, + onChatItemEngagement: this.connector.triggerSuggestionEngagement, + onSourceLinkClick: (tabId, messageId, link, mouseEvent) => { + mouseEvent?.preventDefault() + mouseEvent?.stopPropagation() + mouseEvent?.stopImmediatePropagation() + this.connector?.onSourceLinkClick(tabId, messageId, link) + }, + onLinkClick: (tabId, messageId, link, mouseEvent) => { + mouseEvent?.preventDefault() + mouseEvent?.stopPropagation() + mouseEvent?.stopImmediatePropagation() + this.connector?.onResponseBodyLinkClick(tabId, messageId, link) + }, + onInfoLinkClick: (tabId: string, link: string, mouseEvent?: MouseEvent) => { + mouseEvent?.preventDefault() + mouseEvent?.stopPropagation() + mouseEvent?.stopImmediatePropagation() + this.connector?.onInfoLinkClick(tabId, link) + }, + onResetStore: () => {}, + onFollowUpClicked: (tabID, messageId, followUp) => { + this.followUpsInteractionHandler!.onFollowUpClicked(tabID, messageId, followUp) + }, + onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => { + this.connector?.onFileActionClick(tabID, messageId, filePath, actionName) + }, + onFileClick: this.connector?.onFileClick, + onChatPromptProgressActionButtonClicked: (tabID, action) => { + this.connector?.onCustomFormAction(tabID, undefined, action) + }, + tabs: { + 'tab-1': { + isSelected: true, + store: { + ...(showWelcomePage + ? welcomeScreenTabData(this.tabDataGenerator).store + : this.tabDataGenerator.getTabData('cwc', true)), + ...(this.disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), + }, }, }, - }, - onInBodyButtonClicked: (tabId, messageId, action, eventId) => { - if (action.id === disclaimerAcknowledgeButtonId) { - disclaimerCardActive = false - // post message to tell IDE that disclaimer is acknowledged - ideApi.postMessage({ - command: 'disclaimer-acknowledged', - }) - - // create telemetry - ideApi.postMessage(createClickTelemetry('amazonq-disclaimer-acknowledge-button')) - - // remove all disclaimer cards from all tabs - Object.keys(mynahUI.getAllTabs()).forEach((storeTabKey) => { - // eslint-disable-next-line no-null/no-null - mynahUI.updateStore(storeTabKey, { promptInputStickyCard: null }) - }) - } + onInBodyButtonClicked: (tabId, messageId, action, eventId) => { + if (action.id === disclaimerAcknowledgeButtonId) { + this.disclaimerCardActive = false + // post message to tell IDE that disclaimer is acknowledged + postMessage.postMessage({ + command: 'disclaimer-acknowledged', + }) - if (action.id === 'quick-start') { - /** - * quick start is the action on the welcome page. When its - * clicked it collapses the view and puts it into regular - * "chat" which is cwc - */ - tabsStorage.updateTabTypeFromUnknown(tabId, 'cwc') - - // show quick start in the current tab instead of a new one - mynahUI.updateStore(tabId, { - tabHeaderDetails: undefined, - compactMode: false, - tabBackground: false, - promptInputText: '/', - promptInputLabel: undefined, - chatItems: [], - }) + // create telemetry + postMessage.postMessage(createClickTelemetry('amazonq-disclaimer-acknowledge-button')) - ideApi.postMessage(createClickTelemetry('amazonq-welcome-quick-start-button')) - return - } + // remove all disclaimer cards from all tabs + Object.keys(this.mynahUI?.getAllTabs() ?? []).forEach((storeTabKey) => { + // eslint-disable-next-line no-null/no-null + this.mynahUI?.updateStore(storeTabKey, { promptInputStickyCard: null }) + }) + } - if (action.id === 'explore') { - const newTabId = mynahUI.updateStore('', agentWalkthroughDataModel) - if (newTabId === undefined) { - mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, + if (action.id === 'quick-start') { + /** + * quick start is the action on the welcome page. When its + * clicked it collapses the view and puts it into regular + * "chat" which is cwc + */ + this.tabsStorage.updateTabTypeFromUnknown(tabId, 'cwc') + + // show quick start in the current tab instead of a new one + this.mynahUI?.updateStore(tabId, { + tabHeaderDetails: undefined, + compactMode: false, + tabBackground: false, + promptInputText: '/', + promptInputLabel: undefined, + chatItems: [], }) + + postMessage.postMessage(createClickTelemetry('amazonq-welcome-quick-start-button')) return } - tabsStorage.updateTabTypeFromUnknown(newTabId, 'agentWalkthrough') - ideApi.postMessage(createClickTelemetry('amazonq-welcome-explore-button')) - return - } - connector.onCustomFormAction(tabId, messageId, action, eventId) - }, - defaults: { - store: tabDataGenerator.getTabData('cwc', true), - }, - config: { - maxTabs: 10, - feedbackOptions: feedbackOptions, - texts: uiComponentsTexts, - }, - }) + if (action.id === 'explore') { + const newTabId = this.mynahUI?.updateStore('', agentWalkthroughDataModel) + if (newTabId === undefined) { + this.mynahUI?.notify({ + content: uiComponentsTexts.noMoreTabsTooltip, + type: NotificationType.WARNING, + }) + return + } + this.tabsStorage.updateTabTypeFromUnknown(newTabId, 'agentWalkthrough') + postMessage.postMessage(createClickTelemetry('amazonq-welcome-explore-button')) + return + } - followUpsInteractionHandler = new FollowUpInteractionHandler({ - mynahUI, - connector, - tabsStorage, - }) - quickActionHandler = new QuickActionHandler({ - mynahUI, - connector, - tabsStorage, - isFeatureDevEnabled, - isCodeTransformEnabled, - isDocEnabled, - isCodeScanEnabled, - isCodeTestEnabled, - }) - textMessageHandler = new TextMessageHandler({ - mynahUI, - connector, - tabsStorage, - }) - messageController = new MessageController({ - mynahUI, - connector, - tabsStorage, - isFeatureDevEnabled, - isCodeTransformEnabled, - isDocEnabled, - isCodeScanEnabled, - isCodeTestEnabled, - }) + this.connector?.onCustomFormAction(tabId, messageId, action, eventId) + }, + defaults: { + store: this.tabDataGenerator.getTabData('cwc', true), + }, + config: { + maxTabs: 10, + feedbackOptions: feedbackOptions, + texts: uiComponentsTexts, + }, + } + if (!hybridChat) { + /** + * when in hybrid chat the reference gets resolved later so we + * don't need to create mynah UI + */ + this.mynahUIRef = { mynahUI: new MynahUI({ ...this.mynahUIProps, loadStyles: false }) } + } + this.followUpsInteractionHandler = new FollowUpInteractionHandler({ + mynahUIRef: this.mynahUIRef, + connector: this.connector, + tabsStorage: this.tabsStorage, + }) + + this.textMessageHandler = new TextMessageHandler({ + mynahUIRef: this.mynahUIRef, + connector: this.connector, + tabsStorage: this.tabsStorage, + }) + this.messageController = new MessageController({ + mynahUIRef: this.mynahUIRef, + connector: this.connector, + tabsStorage: this.tabsStorage, + isFeatureDevEnabled, + isCodeTransformEnabled, + isDocEnabled, + isCodeScanEnabled, + isCodeTestEnabled, + }) + this.quickActionHandler = new QuickActionHandler({ + mynahUIRef: this.mynahUIRef, + connector: this.connector!, + tabsStorage: this.tabsStorage, + isFeatureDevEnabled: this.isFeatureDevEnabled, + isCodeTransformEnabled: this.isCodeTransformEnabled, + isDocEnabled: this.isDocEnabled, + isCodeScanEnabled: this.isCodeScanEnabled, + isCodeTestEnabled: this.isCodeTestEnabled, + hybridChat + }) + + } + get mynahUI(): MynahUI | undefined { + return this.mynahUIRef.mynahUI + } } + + +export type MynahUIRef = { mynahUI: MynahUI | undefined } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts index 72635f07755..1a0f43b4bfc 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts @@ -8,9 +8,10 @@ import { Connector } from '../connector' import { TabType, TabsStorage } from '../storages/tabsStorage' import { TabDataGenerator } from '../tabs/generator' import { uiComponentsTexts } from '../texts/constants' +import {MynahUIRef} from "../main"; export interface MessageControllerProps { - mynahUI: MynahUI + mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage isFeatureDevEnabled: boolean @@ -21,13 +22,13 @@ export interface MessageControllerProps { } export class MessageController { - private mynahUI: MynahUI + private mynahUIRef: MynahUIRef private connector: Connector private tabsStorage: TabsStorage private tabDataGenerator: TabDataGenerator constructor(props: MessageControllerProps) { - this.mynahUI = props.mynahUI + this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage this.tabDataGenerator = new TabDataGenerator({ @@ -43,12 +44,12 @@ export class MessageController { const selectedTab = { ...this.tabsStorage.getSelectedTab() } if (selectedTab?.id === undefined || selectedTab?.type === 'featuredev') { // Create a new tab if there's none - const newTabID: string | undefined = this.mynahUI.updateStore( + const newTabID: string | undefined = this.mynahUI?.updateStore( '', this.tabDataGenerator.getTabData('cwc', false) ) if (newTabID === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) @@ -62,7 +63,7 @@ export class MessageController { }) selectedTab.id = newTabID } - this.mynahUI.addToUserPrompt(selectedTab.id, message.body as string) + this.mynahUI?.addToUserPrompt(selectedTab.id, message.body as string) return selectedTab.id } @@ -78,13 +79,13 @@ export class MessageController { this.tabsStorage.updateTabStatus(selectedTab.id, 'busy') this.tabsStorage.updateTabTypeFromUnknown(selectedTab.id, tabType) - this.mynahUI.updateStore(selectedTab.id, { + this.mynahUI?.updateStore(selectedTab.id, { loadingChat: true, cancelButtonWhenLoading: false, promptInputDisabledState: true, }) - this.mynahUI.addChatItem(selectedTab.id, message) - this.mynahUI.addChatItem(selectedTab.id, { + this.mynahUI?.addChatItem(selectedTab.id, message) + this.mynahUI?.addChatItem(selectedTab.id, { type: ChatItemType.ANSWER_STREAM, body: '', }) @@ -92,24 +93,24 @@ export class MessageController { return selectedTab.id } - const newTabID: string | undefined = this.mynahUI.updateStore( + const newTabID: string | undefined = this.mynahUI?.updateStore( '', this.tabDataGenerator.getTabData('cwc', false) ) if (newTabID === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) return undefined } else { - this.mynahUI.addChatItem(newTabID, message) - this.mynahUI.addChatItem(newTabID, { + this.mynahUI?.addChatItem(newTabID, message) + this.mynahUI?.addChatItem(newTabID, { type: ChatItemType.ANSWER_STREAM, body: '', }) - this.mynahUI.updateStore(newTabID, { + this.mynahUI?.updateStore(newTabID, { loadingChat: true, cancelButtonWhenLoading: false, promptInputDisabledState: true, @@ -131,4 +132,7 @@ export class MessageController { return newTabID } } + private get mynahUI(): MynahUI | undefined { + return this.mynahUIRef.mynahUI + } } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/handler.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/handler.ts index 92fff3747ba..7f37ba51ff0 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/handler.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/handler.ts @@ -6,20 +6,21 @@ import { ChatItemType, ChatPrompt, MynahUI } from '@aws/mynah-ui-chat' import { Connector } from '../connector' import { TabsStorage } from '../storages/tabsStorage' +import {MynahUIRef} from "../main"; export interface TextMessageHandlerProps { - mynahUI: MynahUI + mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage } export class TextMessageHandler { - private mynahUI: MynahUI + private mynahUIRef: MynahUIRef private connector: Connector private tabsStorage: TabsStorage constructor(props: TextMessageHandlerProps) { - this.mynahUI = props.mynahUI + this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage } @@ -28,12 +29,12 @@ export class TextMessageHandler { this.tabsStorage.updateTabTypeFromUnknown(tabID, 'cwc') this.tabsStorage.resetTabTimer(tabID) this.connector.onUpdateTabType(tabID) - this.mynahUI.addChatItem(tabID, { + this.mynahUI?.addChatItem(tabID, { type: ChatItemType.PROMPT, body: chatPrompt.escapedPrompt, }) - this.mynahUI.updateStore(tabID, { + this.mynahUI?.updateStore(tabID, { loadingChat: true, cancelButtonWhenLoading: false, promptInputDisabledState: true, @@ -48,4 +49,8 @@ export class TextMessageHandler { }) .then(() => {}) } + + private get mynahUI(): MynahUI | undefined { + return this.mynahUIRef.mynahUI + } } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts index 59e959b4717..c18746be1d8 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts @@ -8,9 +8,10 @@ import { TabDataGenerator } from '../tabs/generator' import { Connector } from '../connector' import { Tab, TabsStorage } from '../storages/tabsStorage' import { uiComponentsTexts } from '../texts/constants' +import { MynahUIRef } from "../main"; export interface QuickActionsHandlerProps { - mynahUI: MynahUI + mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage isFeatureDevEnabled: boolean @@ -18,10 +19,11 @@ export interface QuickActionsHandlerProps { isDocEnabled: boolean isCodeScanEnabled: boolean isCodeTestEnabled: boolean + hybridChat?: boolean } export class QuickActionHandler { - private mynahUI: MynahUI + private mynahUIRef: MynahUIRef private connector: Connector private tabsStorage: TabsStorage private tabDataGenerator: TabDataGenerator @@ -30,9 +32,10 @@ export class QuickActionHandler { public isDocEnabled: boolean public isCodeScanEnabled: boolean public isCodeTestEnabled: boolean + private isHybridChatEnabled: boolean constructor(props: QuickActionsHandlerProps) { - this.mynahUI = props.mynahUI + this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage this.tabDataGenerator = new TabDataGenerator({ @@ -47,6 +50,7 @@ export class QuickActionHandler { this.isDocEnabled = props.isDocEnabled this.isCodeScanEnabled = props.isCodeScanEnabled this.isCodeTestEnabled = props.isCodeTestEnabled + this.isHybridChatEnabled = props.hybridChat ?? false } // Entry point for `/xxx` commands @@ -85,10 +89,10 @@ export class QuickActionHandler { // Check for existing opened transform tab const existingTransformTab = this.tabsStorage.getTabs().find((tab) => tab.type === 'codetransform') if (existingTransformTab !== undefined) { - this.mynahUI.selectTab(existingTransformTab.id, eventId || "") + this.mynahUI?.selectTab(existingTransformTab.id, eventId || "") this.connector.onTabChange(existingTransformTab.id) - this.mynahUI.notify({ + this.mynahUI?.notify({ duration: 5000, title: "Q CodeTransformation", content: "Switched to the existing /transform tab; click 'Start a new transformation' below to run another transformation" @@ -97,12 +101,9 @@ export class QuickActionHandler { } // Add new tab - let affectedTabId: string | undefined = tabID - if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') { - affectedTabId = this.mynahUI.updateStore('', {cancelButtonWhenLoading: false}) - } + const affectedTabId: string | undefined = this.addTab(tabID) if (affectedTabId === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) @@ -111,9 +112,9 @@ export class QuickActionHandler { this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'codetransform') this.connector.onKnownTabOpen(affectedTabId) // Clear unknown tab type's welcome message - this.mynahUI.updateStore(affectedTabId, {chatItems: []}) - this.mynahUI.updateStore(affectedTabId, this.tabDataGenerator.getTabData('codetransform', true)) - this.mynahUI.updateStore(affectedTabId, { + this.mynahUI?.updateStore(affectedTabId, {chatItems: []}) + this.mynahUI?.updateStore(affectedTabId, this.tabDataGenerator.getTabData('codetransform', true)) + this.mynahUI?.updateStore(affectedTabId, { promptInputDisabledState: true, promptInputPlaceholder: 'Open a new tab to chat with Q.', loadingChat: true, @@ -127,7 +128,7 @@ export class QuickActionHandler { } private handleClearCommand(tabID: string) { - this.mynahUI.updateStore(tabID, { + this.mynahUI?.updateStore(tabID, { chatItems: [], }) this.connector.clearChat(tabID) @@ -147,13 +148,11 @@ export class QuickActionHandler { return } - let affectedTabId: string | undefined = tabID + const affectedTabId: string | undefined = this.addTab(tabID) const realPromptText = chatPrompt.escapedPrompt?.trim() ?? '' - if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') { - affectedTabId = this.mynahUI.updateStore('', {}) - } + if (affectedTabId === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) @@ -163,14 +162,14 @@ export class QuickActionHandler { this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) - this.mynahUI.updateStore(affectedTabId, { chatItems: [] }) - this.mynahUI.updateStore( + this.mynahUI?.updateStore(affectedTabId, { chatItems: [] }) + this.mynahUI?.updateStore( affectedTabId, this.tabDataGenerator.getTabData('featuredev', false, taskName) ) const addInformationCard = (tabId: string) => { - this.mynahUI.addChatItem(tabId, { + this.mynahUI?.addChatItem(tabId, { type: ChatItemType.ANSWER, informationCard: { title: "Feature development", @@ -192,19 +191,19 @@ export class QuickActionHandler { }; if (realPromptText !== '') { - this.mynahUI.addChatItem(affectedTabId, { + this.mynahUI?.addChatItem(affectedTabId, { type: ChatItemType.PROMPT, body: realPromptText, }) addInformationCard(affectedTabId) - this.mynahUI.addChatItem(affectedTabId, { + this.mynahUI?.addChatItem(affectedTabId, { type: ChatItemType.ANSWER_STREAM, body: '', }) - this.mynahUI.updateStore(affectedTabId, { + this.mynahUI?.updateStore(affectedTabId, { loadingChat: true, promptInputDisabledState: true, }) @@ -223,15 +222,12 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string return } - let affectedTabId: string | undefined = tabID + const affectedTabId: string | undefined = this.addTab(tabID) const realPromptText = chatPrompt.escapedPrompt?.trim() ?? '' - if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') { - affectedTabId = this.mynahUI.updateStore('', {}) - } if (affectedTabId === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) @@ -241,9 +237,9 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) - this.mynahUI.updateStore(affectedTabId, { chatItems: [] }) + this.mynahUI?.updateStore(affectedTabId, { chatItems: [] }) - this.mynahUI.updateStore( + this.mynahUI?.updateStore( affectedTabId, { ...this.tabDataGenerator.getTabData('doc', realPromptText === '', taskName), promptInputDisabledState: true @@ -251,12 +247,12 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string ) if (realPromptText !== '') { - this.mynahUI.addChatItem(affectedTabId, { + this.mynahUI?.addChatItem(affectedTabId, { type: ChatItemType.PROMPT, body: realPromptText, }) - this.mynahUI.updateStore(affectedTabId, { + this.mynahUI?.updateStore(affectedTabId, { loadingChat: true, promptInputDisabledState: true, }) @@ -269,11 +265,11 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string } private showScanInTab( tabId: string) { - this.mynahUI.addChatItem(tabId, { + this.mynahUI?.addChatItem(tabId, { type: ChatItemType.PROMPT, body: "Run a code review", }) - this.mynahUI.addChatItem(tabId, { + this.mynahUI?.addChatItem(tabId, { type: ChatItemType.ANSWER, informationCard: { title: "/review", @@ -299,10 +295,10 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string // Check for existing opened code scan tab const existingCodeScanTab = this.tabsStorage.getTabs().find(tab => tab.type === 'codescan') if (existingCodeScanTab !== undefined ) { - this.mynahUI.selectTab(existingCodeScanTab.id, eventId || "") + this.mynahUI?.selectTab(existingCodeScanTab.id, eventId || "") this.connector.onTabChange(existingCodeScanTab.id) - this.mynahUI.notify({ + this.mynahUI?.notify({ title: "Q - Review", content: "Switched to the opened code review tab" }); @@ -311,12 +307,10 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string } // Add new tab - let affectedTabId: string | undefined = tabID - if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') { - affectedTabId = this.mynahUI.updateStore('', {}) - } + const affectedTabId: string | undefined = this.addTab(tabID) + if (affectedTabId === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING }) @@ -325,9 +319,9 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'codescan') this.connector.onKnownTabOpen(affectedTabId) // Clear unknown tab type's welcome message - this.mynahUI.updateStore(affectedTabId, {chatItems: []}) - this.mynahUI.updateStore(affectedTabId, this.tabDataGenerator.getTabData('codescan', true)) - this.mynahUI.updateStore(affectedTabId, { + this.mynahUI?.updateStore(affectedTabId, {chatItems: []}) + this.mynahUI?.updateStore(affectedTabId, this.tabDataGenerator.getTabData('codescan', true)) + this.mynahUI?.updateStore(affectedTabId, { promptInputDisabledState: true, promptInputPlaceholder: 'Waiting on your inputs...', loadingChat: true, @@ -338,28 +332,31 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string this.showScanInTab(affectedTabId) } - private handleCodeTestCommand(chatPrompt: ChatPrompt, tabID: string, eventId: string | undefined) { + private handleCodeTestCommand(chatPrompt: ChatPrompt, tabID: string | undefined, eventId: string | undefined) { if (!this.isCodeTestEnabled) { return } const testTabId = this.tabsStorage.getTabs().find((tab) => tab.type === 'codetest')?.id const realPromptText = chatPrompt.escapedPrompt?.trim() ?? '' if (testTabId !== undefined) { - this.mynahUI.selectTab(testTabId, eventId || '') + this.mynahUI?.selectTab(testTabId, eventId || '') this.connector.onTabChange(testTabId) this.connector.startTestGen(testTabId, realPromptText) return } - let affectedTabId: string | undefined = tabID + /** + * right click -> generate test has no tab id + * we have to manually create one if a testgen tab + * wasn't previously created + */ + if (!tabID) { + tabID = this.mynahUI?.updateStore('', {}) + } + const affectedTabId: string | undefined = this.addTab(tabID) // if there is no test tab, open a new one - if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') { - affectedTabId = this.mynahUI.updateStore('', { - loadingChat: true, - }) - } if (affectedTabId === undefined) { - this.mynahUI.notify({ + this.mynahUI?.notify({ content: uiComponentsTexts.noMoreTabsTooltip, type: NotificationType.WARNING, }) @@ -369,12 +366,12 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) // reset chat history - this.mynahUI.updateStore(affectedTabId, { + this.mynahUI?.updateStore(affectedTabId, { chatItems: [], }) // creating a new tab and printing some title - this.mynahUI.updateStore( + this.mynahUI?.updateStore( affectedTabId, this.tabDataGenerator.getTabData('codetest', realPromptText === '', 'Q - Test') ) @@ -382,4 +379,36 @@ private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string this.connector.startTestGen(affectedTabId, realPromptText) } } + + // Ref: https://github.com/aws/aws-toolkit-vscode/blob/e9ea8082ffe0b9968a873437407d0b6b31b9e1a5/packages/core/src/amazonq/webview/ui/quickActions/handler.ts#L345 + private addTab(affectedTabId: string | undefined) { + if (!affectedTabId || !this.mynahUI) { + return + } + + const currTab = this.mynahUI.getAllTabs()[affectedTabId] + const currTabWasUsed = + (currTab.store?.chatItems?.filter((item) => item.type === ChatItemType.PROMPT).length ?? 0) > 0 + if (currTabWasUsed) { + affectedTabId = this.mynahUI.updateStore('', { + loadingChat: true, + cancelButtonWhenLoading: false, + }) + } + + if (affectedTabId && this.isHybridChatEnabled) { + this.tabsStorage.addTab({ + id: affectedTabId, + type: 'unknown', + status: 'free', + isSelected: true, + }) + } + + return affectedTabId + } + + private get mynahUI(): MynahUI | undefined { + return this.mynahUIRef.mynahUI + } } diff --git a/plugins/amazonq/mynah-ui/webpack.media.config.js b/plugins/amazonq/mynah-ui/webpack.media.config.js index 978a8369481..c11715ddf97 100644 --- a/plugins/amazonq/mynah-ui/webpack.media.config.js +++ b/plugins/amazonq/mynah-ui/webpack.media.config.js @@ -48,4 +48,47 @@ const config = { ], }, }; -module.exports = config; + +const connectorAdapter = { + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { from: './node_modules/web-tree-sitter/tree-sitter.wasm', to: ''} + ] + }) + ], + target: 'web', + entry: './src/mynah-ui/connectorAdapter.ts', + output: { + path: path.resolve(__dirname, 'build/assets/js'), + filename: 'connectorAdapter.js', + library: 'connectorAdapter', + libraryTarget: 'var', + devtoolModuleFilenameTemplate: '../[resource-path]', + }, + devtool: 'source-map', + resolve: { + extensions: ['.ts', '.js', '.wasm'], + fallback: { + fs: false, + path: false, + util: false + } + }, + experiments: { asyncWebAssembly: true }, + module: { + rules: [ + {test: /\.(sa|sc|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader']}, + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader', + }, + ], + }, + ], + }, +}; +module.exports = [config, connectorAdapter]; diff --git a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts index 315356836b5..fb011ad3990 100644 --- a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts @@ -24,3 +24,12 @@ dependencies { testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community"))) } + +// hack because our test structure currently doesn't make complete sense +tasks.prepareTestSandbox { + val pluginXmlJar = project(":plugin-amazonq").tasks.jar + + dependsOn(pluginXmlJar) + intoChild(intellijPlatform.projectName.map { "$it/lib" }) + .from(pluginXmlJar) +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt b/plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt similarity index 96% rename from plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt rename to plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt index b95002fe19f..4f7cae9cd55 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt @@ -1,4 +1,4 @@ -// 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 migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization 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 0fa47c27024..f3d9dafc8e7 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 @@ -1,8 +1,10 @@ // Copyright 2024 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 +import com.google.gson.Gson import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -17,7 +19,6 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization import software.aws.toolkits.jetbrains.utils.isQExpired - @Service class CodeWhispererFeatureConfigService { private val featureConfigs = mutableMapOf() @@ -82,6 +83,17 @@ class CodeWhispererFeatureConfigService { fun getChatWSContext(): Boolean = getFeatureValueForKey(CHAT_WS_CONTEXT).stringValue() == "TREATMENT" + // convert into mynahUI parsable string + // format: '[["key1", {"name":"Feature1","variation":"A","value":true}]]' + fun getFeatureConfigJsonString(): String { + val jsonString = featureConfigs.entries.map { (key, value) -> + "[\"$key\",${Gson().toJson(value)}]" + } + return """ + '$jsonString' + """.trimIndent() + } + // Get the feature value for the given key. // In case of a misconfiguration, it will return a default feature value of Boolean false. private fun getFeatureValueForKey(name: String): FeatureValue = diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt index f6dea5c681e..55b6c46d7ef 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt @@ -3,7 +3,9 @@ package software.aws.toolkits.jetbrains.services.amazonq +import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.project.Project +import com.intellij.openapi.util.BuildNumber import com.intellij.openapi.util.SystemInfo import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory import software.amazon.awssdk.services.codewhispererruntime.model.OperatingSystem @@ -52,3 +54,12 @@ fun codeWhispererUserContext(): UserContext = ClientMetadata.getDefault().let { .ideVersion(it.awsVersion) .build() } + +fun isQSupportedInThisVersion(): Boolean { + val currentBuild = ApplicationInfo.getInstance().build.withoutProductCode() + + return !( + currentBuild.baselineVersion == 242 && + BuildNumber.fromString("242.22855.74")?.let { currentBuild < it } == true + ) +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQChatServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQChatServer.kt new file mode 100644 index 00000000000..c1be2d75130 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQChatServer.kt @@ -0,0 +1,210 @@ +// 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.json.JsonRpcMethodProvider +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ButtonClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ButtonClickResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_BUTTON_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_CONVERSATION_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_COPY_CODE_TO_CLIPBOARD_NOTIFICATION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_CREATE_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FEEDBACK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FILE_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_FOLLOW_UP_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_INFO_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_INSERT_TO_CURSOR_NOTIFICATION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_LIST_CONVERSATIONS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_QUICK_ACTION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_READY +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SOURCE_LINK_CLICK +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_ADD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_BAR_ACTIONS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_CHANGE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ConversationClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyCodeToClipboardParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CreatePromptParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FeedbackParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FileClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FollowUpClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.InfoLinkClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.InsertToCursorPositionParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LinkClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ListConversationsParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.PROMPT_INPUT_OPTIONS_CHANGE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.PromptInputOptionChangeParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SourceLinkClickParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TELEMETRY_EVENT +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TabBarActionParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TabEventParams +import kotlin.reflect.KProperty +import kotlin.reflect.full.declaredMembers + +sealed interface JsonRpcMethod { + val name: String + val params: Class +} + +data class JsonRpcNotification( + override val name: String, + override val params: Class, +) : JsonRpcMethod + +@Suppress("FunctionNaming") +fun JsonRpcNotification(name: String) = JsonRpcNotification(name, Unit::class.java) + +data class JsonRpcRequest( + override val name: String, + override val params: Class, + val response: Class, +) : JsonRpcMethod + +/** + * Messaging for the Chat feature follows this pattern: + * Mynah-UI <-> Plugin <-> Flare LSP + * + * However, the default scenario is that the plugin only cares about a subset of request/response payload and should otherwise transparently passthrough data. + * To obtain some semblance of type safety, we model the subset of values that are relevant and passthrough the rest. + * + * Generally, methods MUST be modeled here if the response type is needed, or LSP4J will return null + */ +object AmazonQChatServer : JsonRpcMethodProvider { + override fun supportedMethods() = buildMap { + AmazonQChatServer::class.declaredMembers.filter { it is KProperty }.forEach { + val method = it.call(AmazonQChatServer) as JsonRpcMethod<*, *> + + // trick lsp4j into returning the complete message even if we didn't model it completely + val lsp4jMethod = when (method) { + is JsonRpcNotification<*> -> org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod.notification(method.name, Any::class.java) + is JsonRpcRequest<*, *> -> org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod.request(method.name, Any::class.java, Any::class.java) + } + + put(method.name, lsp4jMethod) + } + } + + val sendChatPrompt = JsonRpcRequest( + SEND_CHAT_COMMAND_PROMPT, + EncryptedChatParams::class.java, + String::class.java + ) + + val sendQuickAction = JsonRpcRequest( + CHAT_QUICK_ACTION, + EncryptedQuickActionChatParams::class.java, + String::class.java + ) + + val copyCodeToClipboard = JsonRpcNotification( + CHAT_COPY_CODE_TO_CLIPBOARD_NOTIFICATION, + CopyCodeToClipboardParams::class.java, + ) + + val chatReady = JsonRpcNotification( + CHAT_READY, + ) + + val tabAdd = JsonRpcNotification( + CHAT_TAB_ADD, + TabEventParams::class.java + ) + + val tabRemove = JsonRpcNotification( + CHAT_TAB_REMOVE, + TabEventParams::class.java + ) + + val tabChange = JsonRpcNotification( + CHAT_TAB_CHANGE, + TabEventParams::class.java + ) + + val feedback = JsonRpcNotification( + CHAT_FEEDBACK, + FeedbackParams::class.java + ) + + val insertToCursorPosition = JsonRpcNotification( + CHAT_INSERT_TO_CURSOR_NOTIFICATION, + InsertToCursorPositionParams::class.java + ) + + val linkClick = JsonRpcNotification( + CHAT_LINK_CLICK, + LinkClickParams::class.java + ) + + val infoLinkClick = JsonRpcNotification( + CHAT_INFO_LINK_CLICK, + InfoLinkClickParams::class.java + ) + + val sourceLinkClick = JsonRpcNotification( + CHAT_SOURCE_LINK_CLICK, + SourceLinkClickParams::class.java + ) + + val promptInputOptionsChange = JsonRpcNotification( + PROMPT_INPUT_OPTIONS_CHANGE, + PromptInputOptionChangeParams::class.java + ) + + val followUpClick = JsonRpcNotification( + CHAT_FOLLOW_UP_CLICK, + FollowUpClickParams::class.java + ) + + val fileClick = JsonRpcNotification( + CHAT_FILE_CLICK, + FileClickParams::class.java + ) + + val listConversations = JsonRpcRequest( + CHAT_LIST_CONVERSATIONS, + ListConversationsParams::class.java, + Any::class.java + ) + + val conversationClick = JsonRpcRequest( + CHAT_CONVERSATION_CLICK, + ConversationClickParams::class.java, + Any::class.java + ) + + val buttonClick = JsonRpcRequest( + CHAT_BUTTON_CLICK, + ButtonClickParams::class.java, + ButtonClickResult::class.java + ) + + val tabBarActions = JsonRpcRequest( + CHAT_TAB_BAR_ACTIONS, + TabBarActionParams::class.java, + Any::class.java + ) + + val getSerializedActions = JsonRpcRequest( + GET_SERIALIZED_CHAT_REQUEST_METHOD, + GetSerializedChatParams::class.java, + GetSerializedChatResult::class.java + ) + + val createPrompt = JsonRpcNotification( + CHAT_CREATE_PROMPT, + CreatePromptParams::class.java + ) + + val telemetryEvent = JsonRpcNotification( + TELEMETRY_EVENT, + Any::class.java + ) +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQDiffVirtualFile.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQDiffVirtualFile.kt new file mode 100644 index 00000000000..5a59499b286 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQDiffVirtualFile.kt @@ -0,0 +1,37 @@ +// 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.diff.chains.SimpleDiffRequestChain +import com.intellij.diff.editor.ChainDiffVirtualFile +import com.intellij.diff.editor.DiffEditorTabFilesManager +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import software.aws.toolkits.resources.message + +/** + * A virtual file that represents an AmazonQ diff view. + * This class allows us to identify diff files created by AmazonQ. + */ +class AmazonQDiffVirtualFile( + diffChain: SimpleDiffRequestChain, + name: String, +) : ChainDiffVirtualFile(diffChain, name) { + companion object { + fun openDiff(project: Project, diffRequest: SimpleDiffRequest) { + // Find any existing AmazonQ diff files + val fileEditorManager = FileEditorManager.getInstance(project) + val existingDiffFiles = fileEditorManager.openFiles.filterIsInstance() + + // Close existing diff files + existingDiffFiles.forEach { fileEditorManager.closeFile(it) } + + // Create and open the new diff file + val diffChain = SimpleDiffRequestChain(diffRequest) + val diffVirtualFile = AmazonQDiffVirtualFile(diffChain, diffRequest.title ?: message("aws.q.lsp.client.diff_message")) + DiffEditorTabFilesManager.getInstance(project).showDiffFile(diffVirtualFile, true) + } + } +} 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 index 8932881568f..c5141c4446b 100644 --- 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 @@ -3,8 +3,28 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.jsonrpc.services.JsonRequest import org.eclipse.lsp4j.services.LanguageClient +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPEN_TAB +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPTIONS_UPDATE_NOTIFICATION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_CONTEXT_COMMANDS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_UPDATE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyFileParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DID_APPEND_FILE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DID_COPY_FILE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DID_CREATE_DIRECTORY +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DID_REMOVE_FILE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DID_WRITE_FILE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FileParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIFF +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenFileDiffParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata import java.util.concurrent.CompletableFuture @@ -15,4 +35,40 @@ import java.util.concurrent.CompletableFuture interface AmazonQLanguageClient : LanguageClient { @JsonRequest("aws/credentials/getConnectionMetadata") fun getConnectionMetadata(): CompletableFuture + + @JsonRequest(CHAT_OPEN_TAB) + fun openTab(params: LSPAny): CompletableFuture + + @JsonRequest(SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD) + fun showSaveFileDialog(params: ShowSaveFileDialogParams): CompletableFuture + + @JsonRequest(GET_SERIALIZED_CHAT_REQUEST_METHOD) + fun getSerializedChat(params: LSPAny): CompletableFuture + + @JsonNotification(CHAT_SEND_UPDATE) + fun sendChatUpdate(params: LSPAny): CompletableFuture + + @JsonNotification(OPEN_FILE_DIFF) + fun openFileDiff(params: OpenFileDiffParams): CompletableFuture + + @JsonNotification(CHAT_SEND_CONTEXT_COMMANDS) + fun sendContextCommands(params: LSPAny): CompletableFuture + + @JsonNotification(DID_COPY_FILE) + fun copyFile(params: CopyFileParams) + + @JsonNotification(DID_WRITE_FILE) + fun writeFile(params: FileParams) + + @JsonNotification(DID_APPEND_FILE) + fun appendFile(params: FileParams) + + @JsonNotification(DID_REMOVE_FILE) + fun removeFile(params: FileParams) + + @JsonNotification(DID_CREATE_DIRECTORY) + fun createDirectory(params: FileParams) + + @JsonNotification(CHAT_OPTIONS_UPDATE_NOTIFICATION) + fun sendChatOptionsUpdate(params: LSPAny) } 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 index 50b1be3626d..577b51cf612 100644 --- 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 @@ -1,30 +1,110 @@ // 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.intellij.diff.DiffContentFactory +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.ide.BrowserUtil import com.intellij.notification.NotificationType +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.openapi.fileChooser.FileSaverDescriptor +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFileManager +import migration.software.aws.toolkits.jetbrains.settings.AwsSettings import org.eclipse.lsp4j.ConfigurationParams import org.eclipse.lsp4j.MessageActionItem import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.ProgressParams import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.ShowDocumentParams +import org.eclipse.lsp4j.ShowDocumentResult import org.eclipse.lsp4j.ShowMessageRequestParams -import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode +import org.slf4j.event.Level +import software.amazon.awssdk.utils.UserHomeDirectoryUtils +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.core.utils.warn 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.flareChat.AsyncChatUiListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPEN_TAB +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPTIONS_UPDATE_NOTIFICATION +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_CONTEXT_COMMANDS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_UPDATE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyFileParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FileParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenFileDiffParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult 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.services.amazonq.lsp.util.TelemetryParsingUtil +import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings +import software.aws.toolkits.jetbrains.utils.getCleanedContent +import software.aws.toolkits.jetbrains.utils.notify +import software.aws.toolkits.resources.message +import java.io.File +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.util.UUID import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit /** * Concrete implementation of [AmazonQLanguageClient] to handle messages sent from server */ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageClient { + + private fun handleTelemetryMap(telemetryMap: Map<*, *>) { + try { + val name = telemetryMap["name"] as? String ?: return + + @Suppress("UNCHECKED_CAST") + val data = telemetryMap["data"] as? Map ?: return + + TelemetryService.getInstance().record(project) { + datum(name) { + unit(TelemetryParsingUtil.parseMetricUnit(telemetryMap["unit"])) + value(telemetryMap["value"] as? Double ?: 1.0) + passive(telemetryMap["passive"] as? Boolean ?: false) + + telemetryMap["result"]?.let { result -> + metadata("result", result.toString()) + } + + data.forEach { (key, value) -> + metadata(key, value?.toString() ?: "null") + } + } + } + } catch (e: Exception) { + LOG.warn(e) { "Failed to process telemetry event: $telemetryMap" } + } + } + override fun telemetryEvent(`object`: Any) { - println(`object`) + when (`object`) { + is Map<*, *> -> handleTelemetryMap(`object`) + else -> LOG.warn { "Unexpected telemetry event: $`object`" } + } } override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) { @@ -37,7 +117,8 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC MessageType.Warning -> NotificationType.WARNING MessageType.Info, MessageType.Log -> NotificationType.INFORMATION } - println("$type: ${messageParams.message}") + + notify(type, message("q.window.title"), getCleanedContent(messageParams.message, true), project, emptyList()) } override fun showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture? { @@ -47,7 +128,55 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC } override fun logMessage(message: MessageParams) { - showMessage(message) + val type = when (message.type) { + MessageType.Error -> Level.ERROR + MessageType.Warning -> Level.WARN + MessageType.Info, MessageType.Log -> Level.INFO + } + + if (type == Level.ERROR && + message.message.lineSequence().firstOrNull()?.contains("NOTE: The AWS SDK for JavaScript (v2) is in maintenance mode.") == true + ) { + LOG.info { "Suppressed Flare AWS JS SDK v2 EoL error message" } + return + } + + LOG.atLevel(type).log(message.message) + } + + override fun showDocument(params: ShowDocumentParams): CompletableFuture { + try { + if (params.uri.isNullOrEmpty()) { + return CompletableFuture.completedFuture(ShowDocumentResult(false)) + } + + if (params.external == true) { + BrowserUtil.open(params.uri) + return CompletableFuture.completedFuture(ShowDocumentResult(true)) + } + + // The filepath sent by the server contains unicode characters which need to be + // decoded for JB file handling APIs to be handle to handle file operations + val fileToOpen = URLDecoder.decode(params.uri, StandardCharsets.UTF_8.name()) + return CompletableFuture.supplyAsync( + { + try { + val virtualFile = VirtualFileManager.getInstance().refreshAndFindFileByUrl(fileToOpen) + ?: throw IllegalArgumentException("Cannot find file: $fileToOpen") + + FileEditorManager.getInstance(project).openFile(virtualFile, true) + ShowDocumentResult(true) + } catch (e: Exception) { + LOG.warn { "Failed to show document: $fileToOpen" } + ShowDocumentResult(false) + } + }, + ApplicationManager.getApplication()::invokeLater + ) + } catch (e: Exception) { + LOG.warn { "Error showing document" } + return CompletableFuture.completedFuture(ShowDocumentResult(false)) + } } override fun getConnectionMetadata(): CompletableFuture = @@ -55,20 +184,81 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC 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) - ) - } + connection?.let { ConnectionMetadata.fromConnection(it) } + } + + override fun openTab(params: LSPAny): CompletableFuture { + val requestId = UUID.randomUUID().toString() + val result = CompletableFuture() + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.addTabOpenRequest(requestId, result) + + chatManager.notifyUi( + FlareUiMessage( + command = CHAT_OPEN_TAB, + params = params, + requestId = requestId, + ) + ) + + result.orTimeout(30000, TimeUnit.MILLISECONDS) + .whenComplete { _, error -> + chatManager.removeTabOpenRequest(requestId) } + + return result + } + + override fun showSaveFileDialog(params: ShowSaveFileDialogParams): CompletableFuture { + val filters = mutableListOf() + val formatMappings = mapOf("markdown" to "md", "html" to "html") + + params.supportedFormats.forEach { format -> + formatMappings[format]?.let { filters.add(it) } } + val defaultUri = params.defaultUri ?: "export-chat.md" + val saveAtUri = defaultUri.substring(defaultUri.lastIndexOf("/") + 1) + return CompletableFuture.supplyAsync( + { + val descriptor = FileSaverDescriptor("Export", "Choose a location to export").apply { + withFileFilter { file -> + filters.any { ext -> + file.name.endsWith(".$ext") + } + } + } + + val chosenFile = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, project).save(saveAtUri) + + chosenFile?.let { + ShowSaveFileDialogResult(chosenFile.file.path) + } ?: throw ResponseErrorException(ResponseError(ResponseErrorCode.RequestCancelled, "Export cancelled by user", null)) + }, + ApplicationManager.getApplication()::invokeLater + ) + } + + override fun getSerializedChat(params: LSPAny): CompletableFuture { + val requestId = UUID.randomUUID().toString() + val result = CompletableFuture() + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.addSerializedChatRequest(requestId, result) + + chatManager.notifyUi( + FlareUiMessage( + command = GET_SERIALIZED_CHAT_REQUEST_METHOD, + params = params, + requestId = requestId, + ) + ) + + result.orTimeout(30000, TimeUnit.MILLISECONDS) + .whenComplete { _, error -> + chatManager.removeSerializedChatRequest(requestId) + } + + return result + } override fun configuration(params: ConfigurationParams): CompletableFuture> { if (params.items.isEmpty()) { @@ -77,14 +267,33 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC return CompletableFuture.completedFuture( buildList { + val qSettings = CodeWhispererSettings.getInstance() 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() + shouldShareData = qSettings.isMetricOptIn(), + shouldShareCodeReferences = qSettings.isIncludeCodeWithReference(), + // server context + shouldEnableWorkspaceContext = qSettings.isWorkspaceContextEnabled() + ) + ) + } + AmazonQLspConstants.LSP_Q_CONFIGURATION_KEY -> { + add( + AmazonQLspConfiguration( + optOutTelemetry = AwsSettings.getInstance().isTelemetryEnabled, + customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn, + // local context + projectContext = ProjectContextConfiguration( + enableLocalIndexing = qSettings.isProjectContextEnabled(), + indexWorkerThreads = qSettings.getProjectContextIndexThreadCount(), + enableGpuAcceleration = qSettings.isProjectContextGpu(), + localIndexing = LocalIndexingConfiguration( + maxIndexSizeMB = qSettings.getProjectContextIndexMaxSize() + ) + ) ) ) } @@ -93,4 +302,151 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC } ) } + + override fun notifyProgress(params: ProgressParams?) { + if (params == null) return + val chatCommunicationManager = ChatCommunicationManager.getInstance(project) + try { + chatCommunicationManager.handlePartialResultProgressNotification(project, params) + } catch (e: Exception) { + LOG.error(e) { "Cannot handle partial chat" } + } + } + + override fun sendChatUpdate(params: LSPAny): CompletableFuture { + AsyncChatUiListener.notifyPartialMessageUpdate( + project, + FlareUiMessage( + command = CHAT_SEND_UPDATE, + params = params, + ) + ) + + return CompletableFuture.completedFuture(Unit) + } + + private fun File.toVirtualFile() = LocalFileSystem.getInstance().findFileByIoFile(this) + + override fun openFileDiff(params: OpenFileDiffParams): CompletableFuture = + CompletableFuture.supplyAsync( + { + var tempPath: java.nio.file.Path? = null + try { + val fileName = Paths.get(params.originalFileUri).fileName.toString() + // Create a temporary virtual file for syntax highlighting + val fileExtension = fileName.substringAfterLast('.', "") + tempPath = Files.createTempFile(null, ".$fileExtension") + val virtualFile = tempPath.toFile() + .also { it.setReadOnly() } + .toVirtualFile() + + val originalContent = params.originalFileContent ?: run { + val sourceFile = File(params.originalFileUri) + if (sourceFile.exists()) sourceFile.readText() else "" + } + + val contentFactory = DiffContentFactory.getInstance() + var isNewFile = false + val (leftContent, rightContent) = when { + params.isDeleted -> { + contentFactory.create(project, originalContent, virtualFile) to + contentFactory.createEmpty() + } + else -> { + val newContent = params.fileContent.orEmpty() + isNewFile = newContent == originalContent + when { + isNewFile -> { + contentFactory.createEmpty() to + contentFactory.create(project, newContent, virtualFile) + } + else -> { + contentFactory.create(project, originalContent, virtualFile) to + contentFactory.create(project, newContent, virtualFile) + } + } + } + } + val diffRequest = SimpleDiffRequest( + "$fileName ${message("aws.q.lsp.client.diff_message")}", + leftContent, + rightContent, + "Original", + when { + params.isDeleted -> "Deleted" + isNewFile -> "Created" + else -> "Modified" + } + ) + + AmazonQDiffVirtualFile.openDiff(project, diffRequest) + } catch (e: Exception) { + LOG.warn { "Failed to open file diff: ${e.message}" } + } finally { + // Clean up the temporary file used for syntax highlight + try { + tempPath?.let { Files.deleteIfExists(it) } + } catch (e: Exception) { + LOG.warn { "Failed to delete temporary file: ${e.message}" } + } + } + }, + ApplicationManager.getApplication()::invokeLater + ) + + override fun sendContextCommands(params: LSPAny): CompletableFuture { + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.notifyUi( + FlareUiMessage( + command = CHAT_SEND_CONTEXT_COMMANDS, + params = params, + ) + ) + return CompletableFuture.completedFuture(Unit) + } + + override fun appendFile(params: FileParams) = refreshVfs(params.path) + + override fun createDirectory(params: FileParams) = refreshVfs(params.path) + + override fun removeFile(params: FileParams) = refreshVfs(params.path) + + override fun writeFile(params: FileParams) = refreshVfs(params.path) + + override fun copyFile(params: CopyFileParams) { + refreshVfs(params.oldPath) + return refreshVfs(params.newPath) + } + + override fun sendChatOptionsUpdate(params: LSPAny) { + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.notifyUi( + FlareUiMessage( + command = CHAT_OPTIONS_UPDATE_NOTIFICATION, + params = params, + ) + ) + } + + private fun refreshVfs(path: String) { + val currPath = Paths.get(path) + if (currPath.startsWith(localHistoryPath)) return + try { + ApplicationManager.getApplication().executeOnPooledThread { + VfsUtil.markDirtyAndRefresh(false, true, true, currPath.toFile()) + } + } catch (e: Exception) { + LOG.warn(e) { "Could not refresh file" } + } + } + + companion object { + val localHistoryPath = Paths.get( + UserHomeDirectoryUtils.userHomeDirectory(), + ".aws", + "amazonq", + "history" + ) + private val LOG = getLogger() + } } 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 index 2396e273f18..6ef5cf818ec 100644 --- 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 @@ -8,10 +8,13 @@ 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.LogInlineCompletionSessionResultsParams 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 software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionListWithReferences +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.textDocument.InlineCompletionWithReferencesParams import java.util.concurrent.CompletableFuture /** @@ -19,6 +22,12 @@ import java.util.concurrent.CompletableFuture */ @Suppress("unused") interface AmazonQLanguageServer : LanguageServer { + @JsonRequest("aws/textDocument/inlineCompletionWithReferences") + fun inlineCompletionWithReferences(params: InlineCompletionWithReferencesParams): CompletableFuture + + @JsonNotification("aws/logInlineCompletionSessionResults") + fun logInlineCompletionSessionResults(params: LogInlineCompletionSessionResultsParams): CompletableFuture + @JsonNotification("aws/didChangeDependencyPaths") fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams): CompletableFuture diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.kt new file mode 100644 index 00000000000..8f0f612a8ae --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.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 com.google.gson.annotations.SerializedName + +data class AmazonQLspConfiguration( + @SerializedName(AmazonQLspConstants.LSP_OPT_OUT_TELEMETRY_CONFIGURATION_KEY) + val optOutTelemetry: Boolean? = null, + + @SerializedName(AmazonQLspConstants.LSP_ENABLE_TELEMETRY_EVENTS_CONFIGURATION_KEY) + val enableTelemetryEvents: Boolean? = null, + + @SerializedName(AmazonQLspConstants.LSP_CUSTOMIZATION_CONFIGURATION_KEY) + val customization: String? = null, + + val projectContext: ProjectContextConfiguration? = null, +) + +data class ProjectContextConfiguration( + val enableLocalIndexing: Boolean? = null, + + val enableGpuAcceleration: Boolean? = null, + + val indexWorkerThreads: Int? = null, + + val localIndexing: LocalIndexingConfiguration? = null, +) + +data class LocalIndexingConfiguration( + val ignoreFilePatterns: List? = null, + val maxFileSizeMB: Int? = null, + val maxIndexSizeMB: Int? = null, + val indexCacheDirPath: String? = null, +) 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 index ca8fffcbb51..c3a7f0ad7db 100644 --- 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 @@ -8,5 +8,10 @@ object AmazonQLspConstants { 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_Q_CONFIGURATION_KEY = "aws.q" + const val LSP_OPT_OUT_TELEMETRY_CONFIGURATION_KEY = "optOutTelemetry" + const val LSP_ENABLE_TELEMETRY_EVENTS_CONFIGURATION_KEY = "enableTelemetryEventsToDestination" + const val LSP_CUSTOMIZATION_CONFIGURATION_KEY = "customization" const val LSP_WORKSPACE_CONTEXT_ENABLED_KEY = "workspaceContext" + const val LSP_PROJECT_CONTEXT_KEY = "projectContext" } 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 index 69c3b3c8939..28c28100f97 100644 --- 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 @@ -1,34 +1,47 @@ // 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.ToNumberPolicy import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil 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.notification.NotificationAction 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.options.ShowSettingsUtil 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 com.intellij.util.net.HttpConfigurable +import com.intellij.util.net.JdkProxyProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred -import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout +import org.apache.http.client.utils.URIBuilder import org.eclipse.lsp4j.ClientCapabilities import org.eclipse.lsp4j.ClientInfo import org.eclipse.lsp4j.DidChangeConfigurationParams @@ -40,30 +53,49 @@ 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.jsonrpc.MessageConsumer +import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint +import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage import org.eclipse.lsp4j.launch.LSPLauncher import org.slf4j.event.Level +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.core.utils.writeText 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.flareChat.AmazonQLspTypeAdapterFactory +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsExtendedInitializeResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider 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.amazonq.profile.QDefaultServiceConfig +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings +import software.aws.toolkits.jetbrains.utils.notifyInfo +import software.aws.toolkits.resources.message +import software.aws.toolkits.telemetry.Telemetry 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.net.Proxy +import java.net.URI import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.seconds // https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java @@ -101,10 +133,22 @@ internal class LSPProcessListener : ProcessListener { @Service(Service.Level.PROJECT) class AmazonQLspService(private val project: Project, private val cs: CoroutineScope) : Disposable { + private val _flowInstance = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val instanceFlow = _flowInstance.asSharedFlow().map { it.languageServer } + private var instance: Deferred val capabilities get() = instance.getCompleted().initializeResult.getCompleted().capabilities + val encryptionManager + get() = instance.getCompleted().encryptionManager + private val heartbeatJob: Job + private val restartTimestamps = ArrayDeque() + private val restartMutex = Mutex() // Separate mutex for restart tracking + + val rawEndpoint + get() = instance.getCompleted().rawEndpoint + // dont allow lsp commands if server is restarting private val mutex = Mutex(false) @@ -114,15 +158,20 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS var attempts = 0 while (attempts < 3) { try { - return@async withTimeout(30.seconds) { + val result = withTimeout(30.seconds) { val instance = AmazonQServerInstance(project, cs).also { Disposer.register(this@AmazonQLspService, it) } // wait for handshake to complete instance.initializeResult.join() - instance + instance.also { + _flowInstance.emit(it) + } } + + // withTimeout can throw + return@async result } catch (e: Exception) { LOG.warn(e) { "Failed to start LSP server" } } @@ -134,9 +183,50 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS init { instance = start() + + // Initialize heartbeat job + heartbeatJob = cs.launch { + while (isActive) { + delay(5.seconds) // Check every 5 seconds + val shouldLoop = checkConnectionStatus() + if (!shouldLoop) { + break + } + } + } + } + + private suspend fun checkConnectionStatus(): Boolean { + try { + val currentInstance = mutex.withLock { instance }.await() + + // Check if the launcher's Future (startListening) is done + // If it's done, that means the connection has been terminated + if (currentInstance.launcherFuture.isDone) { + LOG.debug { "LSP server connection terminated, checking restart limits" } + val canRestart = checkForRemainingRestartAttempts() + if (!canRestart) { + return false + } + LOG.debug { "Restarting LSP server" } + restart() + } else { + LOG.debug { "LSP server is currently running" } + } + } catch (e: Exception) { + LOG.debug(e) { "Connection status check failed, checking restart limits" } + val canRestart = checkForRemainingRestartAttempts() + if (!canRestart) { + return false + } + LOG.debug { "Restarting LSP server" } + restart() + } + return true } override fun dispose() { + heartbeatJob.cancel() } suspend fun restart() = mutex.withLock { @@ -163,6 +253,25 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS instance = start() } + private suspend fun checkForRemainingRestartAttempts(): Boolean = restartMutex.withLock { + val currentTime = System.currentTimeMillis() + + while (restartTimestamps.isNotEmpty() && + currentTime - restartTimestamps.first() > RESTART_WINDOW_MS + ) { + restartTimestamps.removeFirst() + } + + if (restartTimestamps.size < MAX_RESTARTS) { + restartTimestamps.addLast(currentTime) + return true + } + + LOG.info { "Rate limit reached for LSP server restarts. Stop attempting to restart." } + + return false + } + suspend fun execute(runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T { val lsp = withTimeout(10.seconds) { val holder = mutex.withLock { instance }.await() @@ -180,11 +289,17 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS companion object { private val LOG = getLogger() + private const val MAX_RESTARTS = 5 + private const val RESTART_WINDOW_MS = 3 * 60 * 1000 fun getInstance(project: Project) = project.service() + @Deprecated("Easy to accidentally freeze EDT") fun executeIfRunning(project: Project, runnable: AmazonQLspService.(AmazonQLanguageServer) -> T): T? = project.serviceIfCreated()?.executeSync(runnable) + suspend fun asyncExecuteIfRunning(project: Project, runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T? = + project.serviceIfCreated()?.execute(runnable) + fun didChangeConfiguration(project: Project) { executeIfRunning(project) { it.workspaceService.didChangeConfiguration(DidChangeConfigurationParams()) @@ -194,15 +309,18 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS } private class AmazonQServerInstance(private val project: Project, private val cs: CoroutineScope) : Disposable { - private val encryptionManager = JwtEncryptionManager() + val encryptionManager = JwtEncryptionManager() private val launcher: Launcher val languageServer: AmazonQLanguageServer get() = launcher.remoteProxy + val rawEndpoint: RemoteEndpoint + get() = launcher.remoteEndpoint + @Suppress("ForbiddenVoid") - private val launcherFuture: Future + val launcherFuture: Future private val launcherHandler: KillableProcessHandler val initializeResult: Deferred @@ -249,21 +367,77 @@ private class AmazonQServerInstance(private val project: Project, private val cs 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 artifact = runBlocking { service().fetchArtifact(project) }.toAbsolutePath() + + // more network calls + // make assumption that all requests will resolve to the same CA + // also terrible assumption that default endpoint is reachable + val qUri = URI(QDefaultServiceConfig.ENDPOINT) + val extraCaCerts = try { + val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) + + Files.createTempFile("q-extra-ca", ".pem").apply { + writeText( + TrustChainUtil.certsToPem(rtsTrustChain) + ) + } + } catch (e: Exception) { + LOG.info(e) { "Could not resolve trust chain for $qUri, skipping NODE_EXTRA_CA_CERTS" } + null + } + 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", - ) + val nodePath = getNodeRuntimePath(artifact.resolve(node)) + + val cmd = NodeExePatcher.patch(nodePath) + .withParameters( + LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), + "--stdio", + "--set-credentials-encryption-key", + ).withEnvironment( + buildMap { + extraCaCerts?.let { put("NODE_EXTRA_CA_CERTS", it.toAbsolutePath().toString()) } + + val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri) + // log if only socks proxy available + .firstOrNull { it.type() == Proxy.Type.HTTP } + + if (proxy != null) { + val address = proxy.address() + if (address is java.net.InetSocketAddress) { + put( + "HTTPS_PROXY", + URIBuilder("http://${address.hostName}:${address.port}").apply { + val login = HttpConfigurable.getInstance().proxyLogin + if (login != null) { + setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword) + } + }.build().toASCIIString() + ) + } + } + } + ) + .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) launcherHandler = KillableColoredProcessHandler.Silent(cmd) val inputWrapper = LSPProcessListener() launcherHandler.addProcessListener(inputWrapper) launcherHandler.startNotify() - launcher = LSPLauncher.Builder() + launcher = object : LSPLauncher.Builder() { + override fun getSupportedMethods(): Map = + super.getSupportedMethods() + AmazonQChatServer.supportedMethods() + } + .wrapMessages { consumer -> + MessageConsumer { message -> + if (message is ResponseMessage && message.result is AwsExtendedInitializeResult) { + val result = message.result as AwsExtendedInitializeResult + AwsServerCapabilitiesProvider.getInstance(project).setAwsServerCapabilities(result.getAwsServerCapabilities()) + } + consumer?.consume(message) + } + } .setLocalService(AmazonQLanguageClientImpl(project)) .setRemoteInterface(AmazonQLanguageServer::class.java) .configureGson { @@ -272,6 +446,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs // otherwise Gson treats all numbers as double which causes deser issues it.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + it.registerTypeAdapterFactory(AmazonQLspTypeAdapterFactory()) }.traceMessages( PrintWriter( object : StringWriter() { @@ -295,12 +470,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs encryptionManager.writeInitializationPayload(launcherHandler.process.outputStream) val initializeResult = try { - withTimeout(5.seconds) { - languageServer.initialize(createInitializeParams()).await() - } - } catch (_: TimeoutCancellationException) { - LOG.warn { "LSP initialization timed out" } - null + languageServer.initialize(createInitializeParams()).await() } catch (e: Exception) { LOG.warn(e) { "LSP initialization failed" } null @@ -317,20 +487,143 @@ private class AmazonQServerInstance(private val project: Project, private val cs } // invokeOnCompletion results in weird lock/timeout error - initializeResult.asCompletableFuture().handleAsync { r, ex -> + initializeResult.asCompletableFuture().handleAsync { lspInitResult, ex -> if (ex != null) { return@handleAsync } this@AmazonQServerInstance.apply { - DefaultAuthCredentialsService(project, encryptionManager, this) - TextDocumentServiceHandler(project, this) - WorkspaceServiceHandler(project, this) - DefaultModuleDependenciesService(project, this) + DefaultAuthCredentialsService(project, encryptionManager).also { + Disposer.register(this, it) + } + TextDocumentServiceHandler(project).also { + Disposer.register(this, it) + } + WorkspaceServiceHandler(project, lspInitResult).also { + Disposer.register(this, it) + } + DefaultModuleDependenciesService(project).also { + Disposer.register(this, it) + } } } } + /** + * Resolves the path to a valid Node.js runtime in the following order of preference: + * 1. Uses the provided nodePath if it exists and is executable + * 2. Uses user-specified runtime path from LSP settings if available + * 3. Uses system Node.js if version 18+ is available + * 4. Falls back to original nodePath with a notification to configure runtime + * + * @param nodePath The initial Node.js runtime path to check, typically from the artifact directory + * @return Path The resolved Node.js runtime path to use for the LSP server + * + * Side effects: + * - Logs warnings if initial runtime path is invalid + * - Logs info when using alternative runtime path + * - Shows notification to user if no valid Node.js runtime is found + * + * Note: The function will return a path even if no valid runtime is found, but the LSP server + * may fail to start in that case. The caller should handle potential runtime initialization failures. + */ + private fun getNodeRuntimePath(nodePath: Path): Path { + val resolveNodeMetric = { isBundled: Boolean, success: Boolean -> + Telemetry.languageserver.setup.use { + it.id("q") + it.metadata("languageServerSetupStage", "resolveNode") + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledNode", isBundled) + it.success(success) + } + } + + if (Files.exists(nodePath) && Files.isExecutable(nodePath)) { + resolveNodeMetric(true, true) + return nodePath + } + + // use alternative node runtime if it is not found + LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } + // attempt to use user provided node runtime path + val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() + if (!nodeRuntime.isNullOrEmpty()) { + LOG.info { "Using node from $nodeRuntime " } + + resolveNodeMetric(false, true) + return Path.of(nodeRuntime) + } else { + val localNode = locateNodeCommand() + if (localNode != null) { + LOG.info { "Using node from ${localNode.toAbsolutePath()}" } + + resolveNodeMetric(false, true) + return localNode + } + notifyInfo( + "Amazon Q", + message("amazonqFeatureDev.placeholder.node_runtime_message"), + project = project, + listOf( + NotificationAction.create( + message("codewhisperer.actions.open_settings.title") + ) { _, notification -> + ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title")) + }, + NotificationAction.create( + message("codewhisperer.notification.custom.simple.button.got_it") + ) { _, notification -> notification.expire() } + ) + ) + + resolveNodeMetric(false, false) + return nodePath + } + } + + /** + * Locates node executable ≥18 in system PATH. + * Uses IntelliJ's PathEnvironmentVariableUtil to find executables. + * + * @return Path? The absolute path to node ≥18 if found, null otherwise + */ + private fun locateNodeCommand(): Path? { + val exeName = if (SystemInfo.isWindows) "node.exe" else "node" + + return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName) + .asSequence() + .map { it.toPath() } + .filter { Files.isRegularFile(it) && Files.isExecutable(it) } + .firstNotNullOfOrNull { path -> + try { + val process = ProcessBuilder(path.toString(), "--version") + .redirectErrorStream(true) + .start() + + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroy() + null + } else if (process.exitValue() == 0) { + val version = process.inputStream.bufferedReader().readText().trim() + val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull() + + if (majorVersion != null && majorVersion >= 18) { + path.toAbsolutePath() + } else { + LOG.debug { "Node version < 18 found at: $path (version: $version)" } + null + } + } else { + LOG.debug { "Failed to get version from node at: $path" } + null + } + } catch (e: Exception) { + LOG.debug(e) { "Failed to check version for node at: $path" } + null + } + } + } + override fun dispose() { if (!launcherFuture.isDone) { try { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/NodeExePatcher.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/NodeExePatcher.kt new file mode 100644 index 00000000000..a7d3c8a505d --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/NodeExePatcher.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 + +import com.intellij.execution.configurations.GeneralCommandLine +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.info +import java.nio.file.Path + +/** + * Hacky nonsense to support old glibc platforms like AL2 + * @see "https://github.com/microsoft/vscode/issues/231623" + * @see "https://github.com/aws/aws-toolkit-vscode/commit/6081f890bdbb91fcd8b60c4cc0abb65b15d4a38d" + */ +object NodeExePatcher { + const val GLIBC_LINKER_VAR = "VSCODE_SERVER_CUSTOM_GLIBC_LINKER" + const val GLIBC_PATH_VAR = "VSCODE_SERVER_CUSTOM_GLIBC_PATH" + + fun patch(node: Path): GeneralCommandLine { + val linker = System.getenv(GLIBC_LINKER_VAR) + val glibc = System.getenv(GLIBC_PATH_VAR) + val nodePath = node.toAbsolutePath().toString() + + return if (!linker.isNullOrEmpty() && !glibc.isNullOrEmpty()) { + GeneralCommandLine(linker) + .withParameters("--library-path", glibc, nodePath) + .also { + getLogger().info { "Using glibc patch: $it" } + } + } else { + GeneralCommandLine(nodePath) + } + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt new file mode 100644 index 00000000000..ff82c544307 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.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 + +import com.intellij.util.io.DigestUtil +import com.intellij.util.net.JdkProxyProvider +import com.intellij.util.net.ssl.CertificateManager +import org.apache.http.client.methods.RequestBuilder +import org.apache.http.conn.ssl.DefaultHostnameVerifier +import org.apache.http.impl.client.HttpClientBuilder +import org.apache.http.impl.client.SystemDefaultCredentialsProvider +import org.apache.http.impl.conn.SystemDefaultRoutePlanner +import org.jetbrains.annotations.TestOnly +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn +import java.net.URI +import java.security.KeyStore +import java.security.cert.CertPathBuilder +import java.security.cert.CertStore +import java.security.cert.Certificate +import java.security.cert.CollectionCertStoreParameters +import java.security.cert.PKIXBuilderParameters +import java.security.cert.PKIXCertPathBuilderResult +import java.security.cert.X509CertSelector +import java.security.cert.X509Certificate +import java.util.Base64 +import kotlin.collections.ifEmpty + +object TrustChainUtil { + private val LOG = getLogger() + + @TestOnly + fun resolveTrustChain(certs: Collection, trustAnchors: Collection) = resolveTrustChain( + certs, + keystoreFromCertificates(trustAnchors) + ) + + /** + * Build and validate the complete certificate chain + * @param certs The end-entity certificate + * @param trustAnchors The truststore containing trusted CA certificates + * @return The complete certificate chain + */ + fun resolveTrustChain(certs: Collection, trustAnchors: KeyStore): List { + try { + // Create the selector for the certificate + val selector = X509CertSelector() + selector.certificate = certs.first() + + // Create the parameters for path validation + val pkixParams = PKIXBuilderParameters(trustAnchors, selector) + + // Disable CRL checking since we just want to build the path + pkixParams.isRevocationEnabled = false + + // Create a CertStore containing the certificate we want to validate + val ccsp = CollectionCertStoreParameters(certs) + val certStore = CertStore.getInstance("Collection", ccsp) + pkixParams.addCertStore(certStore) + + // Get the certification path + val builder = CertPathBuilder.getInstance("PKIX") + val result = builder.build(pkixParams) as PKIXCertPathBuilderResult + val certPath = result.certPath + val chain = (certPath.certificates as List).toMutableList() + + // Add the trust anchor (root CA) to complete the chain + val trustAnchorCert = result.trustAnchor.trustedCert + if (trustAnchorCert != null) { + chain.add(trustAnchorCert) + } + + return chain + } catch (e: Exception) { + // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS + LOG.warn(e) { "Could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" } + + return emptyList() + } + } + + fun getTrustChain(uri: URI): List { + val proxyProvider = JdkProxyProvider.getInstance() + var peerCerts: Array = emptyArray() + val verifierDelegate = DefaultHostnameVerifier() + val client = HttpClientBuilder.create() + .setRoutePlanner(SystemDefaultRoutePlanner(proxyProvider.proxySelector)) + .setDefaultCredentialsProvider(SystemDefaultCredentialsProvider()) + .setSSLHostnameVerifier { hostname, sslSession -> + peerCerts = sslSession.peerCertificates + + verifierDelegate.verify(hostname, sslSession) + } + // prompt user via modal to accept certificate if needed; otherwise need to prompt separately prior to launching flare + .setSSLContext(CertificateManager.getInstance().sslContext) + + // client request will fail if user did not accept cert + client.build().use { it.execute(RequestBuilder.options(uri).build()) } + + val certificates = peerCerts as Array + + // java default + custom system + // excluding leaf cert for case where user has both leaf and issuing CA as trusted roots + val allAccepted = CertificateManager.getInstance().trustManager.acceptedIssuers.toSet() - certificates.first() + val ks = keystoreFromCertificates(allAccepted) + + // if this throws then there is a bug because it passed PKIX validation in apache client + val trustChain = try { + resolveTrustChain(certificates.toList(), ks) + } catch (e: Exception) { + // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS + LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not root" } + emptyList() + } + + // if trust chain is empty, then somehow user only trusts the leaf cert??? + return trustChain.ifEmpty { + // so return the served certificate chain from the server and hope that works + certificates.toList() + } + } + + fun certsToPem(certs: List): String = + buildList { + certs.forEach { + add("-----BEGIN CERTIFICATE-----") + add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded)) + add("-----END CERTIFICATE-----") + } + }.joinToString(separator = System.lineSeparator()) + + private fun keystoreFromCertificates(certificates: Collection): KeyStore { + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + ks.load(null, null) + certificates.forEachIndexed { index, cert -> + ks.setCertificateEntry( + cert.subjectX500Principal.toString() + "-" + DigestUtil.sha256Hex(cert.encoded), + cert + ) + } + return ks + } +} 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 index 42c14f59517..dc8d25235dc 100644 --- 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 @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.util.io.createDirectories @@ -16,8 +17,11 @@ 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.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.resources.AwsCoreBundle +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.Telemetry +import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.atomic.AtomicInteger @@ -25,13 +29,13 @@ 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(Paths.get("aws", "toolkits", "language-servers", "AmazonQ")) + private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve(Paths.get("aws", "toolkits", "language-servers", "AmazonQ-JetBrains-temp")) private val logger = getLogger() private const val MAX_DOWNLOAD_ATTEMPTS = 3 } private val currentAttempt = AtomicInteger(0) - fun removeDelistedVersions(delistedVersions: List) { + fun removeDelistedVersions(delistedVersions: List) { val localFolders = getSubFolders(lspArtifactsPath) delistedVersions.forEach { delistedVersion -> @@ -70,7 +74,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, return localFolders .mapNotNull { localFolder -> SemVer.parseFromText(localFolder.fileName.toString())?.let { semVer -> - if (semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion) { + if (semVer >= manifestVersionRanges.startVersion && semVer < manifestVersionRanges.endVersion) { localFolder to semVer } else { null @@ -80,10 +84,10 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, .sortedByDescending { (_, semVer) -> semVer } } - fun getExistingLspArtifacts(versions: List, target: ManifestManager.VersionTarget?): Boolean { - if (versions.isEmpty() || target?.contents == null) return false + fun getExistingLspArtifacts(targetVersion: Version, target: VersionTarget): Boolean { + if (target.contents == null) return false - val localLSPPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString()) + val localLSPPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) if (!localLSPPath.exists()) return false val hasInvalidFiles = target.contents.any { content -> @@ -104,13 +108,13 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, 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()) + suspend fun tryDownloadLspArtifacts(project: Project, targetVersion: Version, target: VersionTarget): Path? { + val destinationPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) while (currentAttempt.get() < maxDownloadAttempts) { currentAttempt.incrementAndGet() logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" } + val temporaryDownloadPath = Files.createTempDirectory("lsp-dl") try { return withBackgroundProgress( @@ -118,14 +122,20 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"), cancellable = true ) { - if (downloadLspArtifacts(temporaryDownloadPath, target) && target != null && !target.contents.isNullOrEmpty()) { - moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath) + if (downloadLspArtifacts(project, temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) { + moveFilesFromSourceToDestination(temporaryDownloadPath, destinationPath) target.contents .mapNotNull { it.filename } - .forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) } - logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" } + .forEach { filename -> extractZipFile(destinationPath.resolve(filename), destinationPath) } + logger.info { "Successfully downloaded and moved LSP artifacts to $destinationPath" } - return@withBackgroundProgress downloadPath + val thirdPartyLicenses = targetVersion.thirdPartyLicenses + logger.info { + "Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $destinationPath. " + + if (thirdPartyLicenses == null) "" else "Attribution notice can be found at $thirdPartyLicenses" + } + + return@withBackgroundProgress destinationPath } return@withBackgroundProgress null @@ -139,7 +149,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } } } temporaryDownloadPath.toFile().deleteRecursively() - downloadPath.toFile().deleteRecursively() + destinationPath.toFile().deleteRecursively() } } logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" } @@ -147,7 +157,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, } @VisibleForTesting - internal fun downloadLspArtifacts(downloadPath: Path, target: ManifestManager.VersionTarget?): Boolean { + internal fun downloadLspArtifacts(project: Project, downloadPath: Path, target: VersionTarget?): Boolean { if (target == null || target.contents.isNullOrEmpty()) { logger.warn { "No target contents available for download" } return false @@ -164,7 +174,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, logger.warn { "No hash available for ${content.filename}" } return@forEach } - downloadAndValidateFile(content.url, filePath, contentHash) + downloadAndValidateFile(project, content.url, filePath, contentHash) } validateDownloadedFiles(downloadPath, target.contents) } catch (e: Exception) { @@ -175,18 +185,46 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, return true } - private fun downloadAndValidateFile(url: String, filePath: Path, expectedHash: String) { + private fun downloadAndValidateFile(project: Project, url: String, filePath: Path, expectedHash: String) { + val recordDownload = { runnable: () -> Unit -> + Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetServer) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) + telemetry.success(true) + + try { + runnable() + } catch (t: Throwable) { + telemetry.success(false) + telemetry.recordException(t) + } + } + } + try { if (!filePath.exists()) { logger.info { "Downloading file: ${filePath.fileName}" } - saveFileFromUrl(url, filePath) + recordDownload { saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) } } 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) + recordDownload { saveFileFromUrl(url, filePath) } + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Validate) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.success(true) + + if (!validateFileHash(filePath, expectedHash)) { + it.success(false) + + val exception = LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH) + it.recordException(exception) + throw exception + } } } } catch (e: Exception) { @@ -201,7 +239,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, return "sha384:$contentHash" == expectedHash } - private fun validateDownloadedFiles(downloadPath: Path, contents: List) { + private fun validateDownloadedFiles(downloadPath: Path, contents: List) { val missingFiles = contents .mapNotNull { it.filename } .filter { filename -> 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 index b74bbde2886..d7970612854 100644 --- 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 @@ -3,33 +3,48 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts +import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project +import com.intellij.serviceContainer.NonInjectable import com.intellij.util.text.SemVer +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock 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 software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.AwsPlugin +import software.aws.toolkits.jetbrains.AwsToolkit +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.MetricResult +import software.aws.toolkits.telemetry.Telemetry import java.nio.file.Path -class ArtifactManager( - private val project: Project, - private val manifestFetcher: ManifestFetcher = ManifestFetcher(), - private val artifactHelper: ArtifactHelper = ArtifactHelper(), - manifestRange: SupportedManifestVersionRange?, -) { +@Service +class ArtifactManager @NonInjectable internal constructor(private val manifestFetcher: ManifestFetcher, private val artifactHelper: ArtifactHelper) { + constructor() : this( + ManifestFetcher(), + ArtifactHelper() + ) + + // we currently cannot handle the versions swithing in the middle of a user's session + private val mutex = Mutex() + private var artifactDeferred: Deferred? = null data class SupportedManifestVersionRange( val startVersion: SemVer, val endVersion: SemVer, ) data class LSPVersions( - val deListedVersions: List, - val inRangeVersions: List, + 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), @@ -38,39 +53,100 @@ class ArtifactManager( 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) + suspend fun fetchArtifact(project: Project): Path { + mutex.withLock { artifactDeferred }?.let { + return it.await() + } - this.artifactHelper.removeDelistedVersions(lspVersions.deListedVersions) + return mutex.withLock { + coroutineScope { + async { + Telemetry.languageserver.setup.use { all -> + all.id("q") + all.languageServerSetupStage(LanguageServerSetupStage.All) + all.metadata("credentialStartUrl", getStartUrl(project)) + all.result(MetricResult.Succeeded) - 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) - } + try { + val lspVersions = Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetManifest) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) - // 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 + val exception = LspException( + "Language Support is not available, as manifest is missing.", + LspException.ErrorCode.MANIFEST_FETCH_FAILED + ) + telemetry.success(true) + val manifest = manifestFetcher.fetch() ?: run { + telemetry.recordException(exception) + telemetry.success(false) + throw exception + } + + getLSPVersionsFromManifestWithSpecifiedRange(manifest) + } + + 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 = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE) + if (localLspArtifacts.isNotEmpty()) { + return@async localLspArtifacts.first().first + } + throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) + } + + val targetVersion = lspVersions.inRangeVersions.first() + + // If there is an LSP Manifest with the same version + val target = getTargetFromLspManifest(targetVersion) + // Get Local LSP files and check if we can re-use existing LSP Artifacts + val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) { + artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first + } else { + artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target) + ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED) + } + + artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", false) + it.success(true) + } + return@async artifactPath + } catch (e: Exception) { + logger.warn(e) { "Failed to resolve assets from Flare CDN" } + val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled") + logger.info { "Falling back to bundled assets at $path" } + + all.recordException(e) + all.result(MetricResult.Failed) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", true) + it.success(false) + } + return@async path + } + } + } + }.also { + artifactDeferred = it + } + }.await() } @VisibleForTesting - internal fun getLSPVersionsFromManifestWithSpecifiedRange(manifest: ManifestManager.Manifest): LSPVersions { + internal fun getLSPVersionsFromManifestWithSpecifiedRange(manifest: Manifest): LSPVersions { if (manifest.versions.isNullOrEmpty()) return LSPVersions(emptyList(), emptyList()) val (deListed, inRange) = manifest.versions.mapNotNull { version -> @@ -78,7 +154,7 @@ class ArtifactManager( 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 + (semVer >= DEFAULT_VERSION_RANGE.startVersion && semVer < DEFAULT_VERSION_RANGE.endVersion) -> Pair(version, false) // Is in range else -> null } } @@ -91,18 +167,18 @@ class ArtifactManager( ) } - private fun getTargetFromLspManifest(versions: List): ManifestManager.VersionTarget { + private fun getTargetFromLspManifest(targetVersion: Version): VersionTarget { val currentOS = getCurrentOS() val currentArchitecture = getCurrentArchitecture() - val currentTarget = versions.first().targets?.find { target -> + val currentTarget = targetVersion.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) + throw LspException("Target not found in the current Version: ${targetVersion.serverVersion}", LspException.ErrorCode.TARGET_NOT_FOUND) } - logger.info { "Target found in the current Version: ${versions.first().serverVersion}" } + logger.info { "Target found in the current Version: ${targetVersion.serverVersion}" } return currentTarget } } 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 index 7724a8c2255..a6846c905a6 100644 --- 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 @@ -7,6 +7,7 @@ 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 org.apache.commons.io.FileUtils import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX import software.aws.toolkits.core.utils.createParentDirectories import software.aws.toolkits.core.utils.exists @@ -68,7 +69,8 @@ fun getSubFolders(basePath: Path): List = try { fun moveFilesFromSourceToDestination(sourceDir: Path, targetDir: Path) { try { Files.createDirectories(targetDir.parent) - Files.move(sourceDir, targetDir, StandardCopyOption.REPLACE_EXISTING) + // NIO move does not work when copying across mount points (i.e. /tmp is on tmpfs) + FileUtils.moveDirectory(sourceDir.toFile(), targetDir.toFile()) } catch (e: Exception) { throw IllegalStateException("Failed to move files from $sourceDir to $targetDir", 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 index 74656d5665b..ea85bf52e52 100644 --- 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 @@ -3,6 +3,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts +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 org.jetbrains.annotations.VisibleForTesting import software.aws.toolkits.core.utils.deleteIfExists import software.aws.toolkits.core.utils.error @@ -10,22 +14,22 @@ 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.core.utils.warn 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 mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } private val logger = getLogger() private const val DEFAULT_MANIFEST_URL = - "https://aws-toolkit-language-servers.amazonaws.com/remoteWorkspaceContext/0/manifest.json" + "https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json" private val DEFAULT_MANIFEST_PATH: Path = getToolkitsCommonCacheRoot() .resolve("aws") @@ -41,7 +45,7 @@ class ManifestFetcher( /** * Method which will be used to fetch latest manifest. * */ - fun fetch(): ManifestManager.Manifest? { + fun fetch(): Manifest? { val localManifest = fetchManifestFromLocal() if (localManifest != null) { return localManifest @@ -50,15 +54,16 @@ class ManifestFetcher( } @VisibleForTesting - internal fun fetchManifestFromRemote(): ManifestManager.Manifest? { - val manifest: ManifestManager.Manifest? + internal fun fetchManifestFromRemote(): Manifest? { + val manifest: Manifest? try { val manifestString = getTextFromUrl(lspManifestUrl) - manifest = manifestManager.readManifestFile(manifestString) ?: return null + manifest = 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 @@ -77,7 +82,7 @@ class ManifestFetcher( } @VisibleForTesting - internal fun fetchManifestFromLocal(): ManifestManager.Manifest? { + internal fun fetchManifestFromLocal(): 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. @@ -85,7 +90,7 @@ class ManifestFetcher( if ((localETag != null && remoteETag != null && localETag == remoteETag) or (localETag != null && remoteETag == null)) { try { val manifestContent = lspManifestFilePath.readText() - val manifest = manifestManager.readManifestFile(manifestContent) + val manifest = readManifestFile(manifestContent) if (manifest != null) return manifest lspManifestFilePath.deleteIfExists() // delete manifest if it fails to de-serialize } catch (e: Exception) { @@ -112,4 +117,57 @@ class ManifestFetcher( } return null } + + fun readManifestFile(content: String): Manifest? { + try { + return mapper.readValue(content) + } catch (e: Exception) { + logger.warn { "error parsing manifest file for project context ${e.message}" } + return null + } + } } + +data class TargetContent( + @JsonProperty("filename") + val filename: String? = null, + @JsonProperty("url") + val url: String? = null, + @JsonProperty("hashes") + val hashes: List? = emptyList(), + @JsonProperty("bytes") + val bytes: Number? = null, +) + +data class VersionTarget( + @JsonProperty("platform") + val platform: String? = null, + @JsonProperty("arch") + val arch: String? = null, + @JsonProperty("contents") + val contents: List? = emptyList(), +) + +data class Version( + @JsonProperty("serverVersion") + val serverVersion: String? = null, + @JsonProperty("isDelisted") + val isDelisted: Boolean? = null, + @JsonProperty("targets") + val targets: List? = emptyList(), + @JsonProperty("thirdPartyLicenses") + val thirdPartyLicenses: String? = null, +) + +data class Manifest( + @JsonProperty("manifestSchemaVersion") + val manifestSchemaVersion: String? = null, + @JsonProperty("artifactId") + val artifactId: String? = null, + @JsonProperty("artifactDescription") + val artifactDescription: String? = null, + @JsonProperty("isManifestDeprecated") + val isManifestDeprecated: Boolean? = null, + @JsonProperty("versions") + val versions: List? = emptyList(), +) 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 index a38c8da4bbc..fb40fb75f35 100644 --- 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 @@ -4,9 +4,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.auth import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import java.util.concurrent.CompletableFuture interface AuthCredentialsService { - fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture + fun updateTokenCredentials(connection: ToolkitConnection, 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 index 5ee8c258d59..873d329600b 100644 --- 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 @@ -22,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryp 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.ConnectionMetadata 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 @@ -37,7 +38,6 @@ import java.util.concurrent.TimeUnit class DefaultAuthCredentialsService( private val project: Project, private val encryptionManager: JwtEncryptionManager, - serverInstance: Disposable, ) : AuthCredentialsService, BearerTokenProviderListener, ToolkitConnectionManagerListener, @@ -46,10 +46,10 @@ class DefaultAuthCredentialsService( private val scheduler: ScheduledExecutorService = AppExecutorUtil.getAppScheduledExecutorService() private var tokenSyncTask: ScheduledFuture<*>? = null - private val tokenSyncIntervalSeconds = 10L + private val tokenSyncIntervalMinutes = 5L init { - project.messageBus.connect(serverInstance).apply { + project.messageBus.connect(this).apply { subscribe(BearerTokenProviderListener.TOPIC, this@DefaultAuthCredentialsService) subscribe(ToolkitConnectionManagerListener.TOPIC, this@DefaultAuthCredentialsService) subscribe(QRegionProfileSelectedListener.TOPIC, this@DefaultAuthCredentialsService) @@ -98,14 +98,18 @@ class DefaultAuthCredentialsService( LOG.warn(e) { "Failed to sync bearer token to Flare" } } }, - tokenSyncIntervalSeconds, - tokenSyncIntervalSeconds, - TimeUnit.SECONDS + tokenSyncIntervalMinutes, + tokenSyncIntervalMinutes, + TimeUnit.MINUTES ) } - override fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture { - val payload = createUpdateCredentialsPayload(accessToken, encrypted) + override fun updateTokenCredentials(connection: ToolkitConnection, encrypted: Boolean): CompletableFuture { + val payload = try { + createUpdateCredentialsPayload(connection, encrypted) + } catch (e: Exception) { + return CompletableFuture.failedFuture(e) + } return AmazonQLspService.executeIfRunning(project) { server -> server.updateTokenCredentials(payload) @@ -142,35 +146,39 @@ class DefaultAuthCredentialsService( } private fun updateTokenFromConnection(connection: ToolkitConnection): CompletableFuture = - (connection.getConnectionSettings() as? TokenConnectionSettings) + updateTokenCredentials(connection, true) + + override fun invalidate(providerId: String) { + deleteTokenCredentials() + } + + private fun createUpdateCredentialsPayload(connection: ToolkitConnection, encrypted: Boolean): UpdateCredentialsPayload { + val token = (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")) + ?: error("Unable to get token from connection") - override fun invalidate(providerId: String) { - deleteTokenCredentials() - } - - private fun createUpdateCredentialsPayload(token: String, encrypted: Boolean): UpdateCredentialsPayload = - if (encrypted) { + return if (encrypted) { UpdateCredentialsPayload( data = encryptionManager.encrypt( UpdateCredentialsPayloadData( BearerCredentials(token) ) ), + metadata = ConnectionMetadata.fromConnection(connection), encrypted = true ) } else { UpdateCredentialsPayload( data = token, + metadata = ConnectionMetadata.fromConnection(connection), encrypted = false ) } + } override fun onProfileSelected(project: Project, profile: QRegionProfile?) { updateConfiguration() 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 index 80239696d14..4f3f4ac8bce 100644 --- 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 @@ -11,16 +11,15 @@ 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 +import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread class DefaultModuleDependenciesService( private val project: Project, - serverInstance: Disposable, ) : ModuleDependenciesService, - ModuleRootListener { - + ModuleRootListener, + Disposable { init { - project.messageBus.connect(serverInstance).subscribe( + project.messageBus.connect(this).subscribe( ModuleRootListener.TOPIC, this ) @@ -34,10 +33,13 @@ class DefaultModuleDependenciesService( syncAllModules() } - override fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams): CompletableFuture = + override fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams) { AmazonQLspService.executeIfRunning(project) { languageServer -> - languageServer.didChangeDependencyPaths(params) - }?.toCompletableFuture() ?: CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")) + pluginAwareExecuteOnPooledThread { + languageServer.didChangeDependencyPaths(params) + } + } + } private fun syncAllModules() { ModuleManager.getInstance(project).modules.forEach { module -> @@ -49,4 +51,7 @@ class DefaultModuleDependenciesService( } } } + + override fun dispose() { + } } 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 index 82370dad895..6cc41eab289 100644 --- 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 @@ -4,8 +4,7 @@ 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 + fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams) } 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 index e8d0087e7d2..af9af6f4eb7 100644 --- 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 @@ -8,7 +8,7 @@ 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 +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil.toUriString interface ModuleDependencyProvider { companion object { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AmazonQLspTypeAdapterFactory.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AmazonQLspTypeAdapterFactory.kt new file mode 100644 index 00000000000..89d76c205c3 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AmazonQLspTypeAdapterFactory.kt @@ -0,0 +1,43 @@ +// 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.flareChat + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import org.eclipse.lsp4j.InitializeResult +import java.io.IOException + +class AmazonQLspTypeAdapterFactory : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType === InitializeResult::class.java) { + val delegate: TypeAdapter = gson.getDelegateAdapter(this, type) as TypeAdapter + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: InitializeResult?) { + delegate.write(out, value) + } + + @Throws(IOException::class) + override fun read(`in`: JsonReader): InitializeResult = + gson.fromJson(`in`, AwsExtendedInitializeResult::class.java) + } as TypeAdapter + } + return null + } +} + +class AwsExtendedInitializeResult(awsServerCapabilities: AwsServerCapabilities? = null) : InitializeResult() { + private var awsServerCapabilities: AwsServerCapabilities? = null + + fun getAwsServerCapabilities(): AwsServerCapabilities? = awsServerCapabilities + + fun setAwsServerCapabilities(awsServerCapabilities: AwsServerCapabilities?) { + this.awsServerCapabilities = awsServerCapabilities + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt new file mode 100644 index 00000000000..21d812cf075 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt @@ -0,0 +1,31 @@ +// 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.flareChat + +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic +import java.util.EventListener + +@Deprecated("Why are we using a message bus for this????????") +interface AsyncChatUiListener : EventListener { + @Deprecated("shouldn't need this version") + fun onChange(command: String) {} + + fun onChange(command: FlareUiMessage) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic.create("Partial chat message provider", AsyncChatUiListener::class.java) + + @Deprecated("Why are we using a message bus for this????????") + fun notifyPartialMessageUpdate(project: Project, command: FlareUiMessage) { + project.messageBus.syncPublisher(TOPIC).onChange(command) + } + + @Deprecated("shouldn't need this version") + fun notifyPartialMessageUpdate(project: Project, command: String) { + project.messageBus.syncPublisher(TOPIC).onChange(command) + } + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AwsServerCapabilitiesProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AwsServerCapabilitiesProvider.kt new file mode 100644 index 00000000000..ad867d062e5 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AwsServerCapabilitiesProvider.kt @@ -0,0 +1,85 @@ +// 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.flareChat + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.IconType + +@Service(Service.Level.PROJECT) +class AwsServerCapabilitiesProvider { + private var serverCapabilities: AwsServerCapabilities? = null + + fun setAwsServerCapabilities(serverCapabilities: AwsServerCapabilities?) { + this.serverCapabilities = serverCapabilities + } + + fun getChatOptions() = serverCapabilities?.chatOptions ?: DEFAULT_CHAT_OPTIONS + + companion object { + fun getInstance(project: Project) = project.service() + + private val DEFAULT_CHAT_OPTIONS: ChatOptions = ChatOptions( + QuickActions( + listOf( + QuickActionsCommandGroups( + listOf( + QuickActionCommand("/help", "Learn more about Amazon Q then"), + QuickActionCommand("/clear", "Clear this session") + ) + ) + ) + ), + history = true, + export = true + ) + } +} + +data class AwsServerCapabilities( + val chatOptions: ChatOptions, +) + +data class ChatOptions( + val quickActions: QuickActions, + val history: Boolean, + val export: Boolean, +) + +data class QuickActions( + val quickActionsCommandGroups: List, +) + +data class QuickActionsCommandGroups( + val commands: List, +) + +open class QuickActionCommand( + open val command: String, + open val description: String?, + open val placeholder: String? = null, + open val icon: IconType? = null, +) + +data class ContextCommand( + val id: String?, + val route: List?, + val label: String?, + val children: ContextCommandGroup?, + override val command: String, + override val description: String?, + override val placeholder: String? = null, + override val icon: IconType? = null, +) : QuickActionCommand( + command = command, + description = description, + placeholder = placeholder, + icon = icon +) + +data class ContextCommandGroup( + val groupName: String?, + val commands: List, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatAsyncResultManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatAsyncResultManager.kt new file mode 100644 index 00000000000..aa729fbf12f --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatAsyncResultManager.kt @@ -0,0 +1,74 @@ +// 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.flareChat + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * Manages asynchronous results for chat operations, particularly handling the coordination + * between partial results and final results during cancellation. + */ +@Service(Service.Level.PROJECT) +class ChatAsyncResultManager { + private val results = ConcurrentHashMap>() + private val completedResults = ConcurrentHashMap() + private val timeout = 30L + private val timeUnit = TimeUnit.SECONDS + + fun createRequestId(requestId: String) { + if (!completedResults.containsKey(requestId)) { + results[requestId] = CompletableFuture() + } + } + + fun removeRequestId(requestId: String) { + val future = results.remove(requestId) + if (future != null && !future.isDone) { + future.cancel(true) + } + completedResults.remove(requestId) + } + + fun setResult(requestId: String, result: Any) { + val future = results[requestId] + if (future != null) { + future.complete(result) + results.remove(requestId) + } + completedResults[requestId] = result + } + + fun getResult(requestId: String): Any? = + getResult(requestId, timeout, timeUnit) + + private fun getResult(requestId: String, timeout: Long, unit: TimeUnit): Any? { + val completedResult = completedResults[requestId] + if (completedResult != null) { + return completedResult + } + + val future = results[requestId] ?: throw IllegalArgumentException("Request ID not found: $requestId") + + try { + val result = future.get(timeout, unit) + completedResults[requestId] = result + results.remove(requestId) + return result + } catch (e: TimeoutException) { + future.cancel(true) + results.remove(requestId) + throw TimeoutException("Operation timed out for requestId: $requestId") + } + } + + companion object { + fun getInstance(project: Project) = project.service() + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt new file mode 100644 index 00000000000..ac7deaa4134 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt @@ -0,0 +1,271 @@ +// 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.flareChat + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.eclipse.lsp4j.ProgressParams +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException +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.credentials.ToolkitConnectionManager +import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection +import software.aws.toolkits.jetbrains.core.credentials.reauthConnectionIfNeeded +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ProgressNotificationUtils.getObject +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AuthFollowUpClickedParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AuthFollowupType +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_ERROR_PARAMS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ErrorParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileDialog +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener +import java.util.UUID +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap + +@Service(Service.Level.PROJECT) +class ChatCommunicationManager(private val project: Project, private val cs: CoroutineScope) { + val uiReady = CompletableDeferred() + private val chatPartialResultMap = ConcurrentHashMap() + private val inflightRequestByTabId = ConcurrentHashMap>() + private val pendingSerializedChatRequests = ConcurrentHashMap>() + private val pendingTabRequests = ConcurrentHashMap>() + private val partialResultLocks = ConcurrentHashMap() + private val finalResultProcessed = ConcurrentHashMap() + + fun setUiReady() { + uiReady.complete(true) + } + + fun notifyUi(uiMessage: FlareUiMessage) { + cs.launch { + uiReady.await() + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) + } + } + + fun setInflightRequestForTab(tabId: String, result: CompletableFuture) { + inflightRequestByTabId[tabId] = result + } + fun removeInflightRequestForTab(tabId: String) { + inflightRequestByTabId.remove(tabId) + } + + fun getInflightRequestForTab(tabId: String): CompletableFuture? = inflightRequestByTabId[tabId] + + fun hasInflightRequest(tabId: String): Boolean = inflightRequestByTabId.containsKey(tabId) + + fun addPartialChatMessage(tabId: String): String { + val partialResultToken: String = UUID.randomUUID().toString() + chatPartialResultMap[partialResultToken] = tabId + return partialResultToken + } + + private fun getPartialChatMessage(partialResultToken: String): String? = + chatPartialResultMap.getOrDefault(partialResultToken, null) + + fun removePartialChatMessage(partialResultToken: String) = + chatPartialResultMap.remove(partialResultToken) + + fun addSerializedChatRequest(requestId: String, result: CompletableFuture) { + pendingSerializedChatRequests[requestId] = result + } + + fun completeSerializedChatResponse(requestId: String, content: String) { + pendingSerializedChatRequests.remove(requestId)?.complete(GetSerializedChatResult((content))) + } + + fun removeSerializedChatRequest(requestId: String) { + pendingSerializedChatRequests.remove(requestId) + } + + fun addTabOpenRequest(requestId: String, result: CompletableFuture) { + pendingTabRequests[requestId] = result + } + + fun removeTabOpenRequest(requestId: String) = + pendingTabRequests.remove(requestId) + + fun removePartialResultLock(token: String) { + partialResultLocks.remove(token) + } + + fun removeFinalResultProcessed(token: String) { + finalResultProcessed.remove(token) + } + + fun registerPartialResultToken(partialResultToken: String) { + val lock = Any() + partialResultLocks[partialResultToken] = lock + finalResultProcessed[partialResultToken] = false + } + + fun handlePartialResultProgressNotification(project: Project, params: ProgressParams) { + val token = ProgressNotificationUtils.getToken(params) + val tabId = getPartialChatMessage(token) + if (tabId.isNullOrEmpty()) { + return + } + if (params.value.isLeft || params.value.right == null) { + error( + "Error handling partial result notification: expected value of type Object" + ) + } + + val encryptedPartialChatResult = getObject(params, String::class.java) + if (encryptedPartialChatResult != null) { + val partialChatResult = AmazonQLspService.getInstance(project).encryptionManager.decrypt(encryptedPartialChatResult) + + // Special case: check for stop message before proceeding + val partialResultMap = tryOrNull { + Gson().fromJson(partialChatResult, Map::class.java) + } + + if (partialResultMap != null) { + @Suppress("UNCHECKED_CAST") + val additionalMessages = partialResultMap["additionalMessages"] as? List> + if (additionalMessages != null) { + for (message in additionalMessages) { + val messageId = message["messageId"] as? String + if (messageId != null && messageId.startsWith("stopped")) { + // Process stop messages immediately + val uiMessage = convertToJsonToSendToChat( + command = SEND_CHAT_COMMAND_PROMPT, + tabId = tabId, + params = partialChatResult, + isPartialResult = true + ) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) + finalResultProcessed[token] = true + ChatAsyncResultManager.getInstance(project).setResult(token, partialResultMap) + return + } + } + } + } + + // Normal processing for non-stop messages + val lock = partialResultLocks[token] ?: return + synchronized(lock) { + if (finalResultProcessed[token] == true || partialResultLocks[token] == null) { + return@synchronized + } + val uiMessage = convertToJsonToSendToChat( + command = SEND_CHAT_COMMAND_PROMPT, + tabId = tabId, + params = partialChatResult, + isPartialResult = true + ) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) + } + } + } + + fun getErrorUiMessage(tabId: String, exception: Exception, token: String?): String { + token?.let { + removePartialChatMessage(it) + } + var errorMessage: String? = null + if (exception is ResponseErrorException) { + errorMessage = tryOrNull { + Gson().fromJson(exception.responseError.data as JsonObject, ChatMessage::class.java).body + } ?: exception.responseError.message + } + + val errorTitle = "An error occurred while processing your request." + errorMessage = errorMessage ?: "Details: ${exception.message}" + val errorParams = Gson().toJson(ErrorParams(tabId, null, errorMessage, errorTitle)).toString() + val isPartialResult = false + val uiMessage = """ + { + "command":"$CHAT_ERROR_PARAMS", + "tabId": "$tabId", + "params": $errorParams, + "isPartialResult": $isPartialResult + } + """.trimIndent() + return uiMessage + } + + fun getCancellationUiMessage(tabId: String): String { + // Create a minimal error params with empty error message to hide the stop button + // without showing an actual error message to the user + val errorParams = Gson().toJson(ErrorParams(tabId, null, "", "")).toString() + + return """ + { + "command":"$CHAT_ERROR_PARAMS", + "tabId": "$tabId", + "params": $errorParams, + "isPartialResult": false + } + """.trimIndent() + } + + fun handleAuthFollowUpClicked(project: Project, params: AuthFollowUpClickedParams) { + val incomingType = params.authFollowupType + val connectionManager = ToolkitConnectionManager.getInstance(project) + try { + when (incomingType) { + AuthFollowupType.USE_SUPPORTED_AUTH -> { + val activeProfile = QRegionProfileManager.getInstance().activeProfile(project) + if (activeProfile != null) { + project.messageBus.syncPublisher(QRegionProfileSelectedListener.TOPIC) + .onProfileSelected(project, QRegionProfileManager.getInstance().activeProfile(project)) + } else { + QRegionProfileDialog( + project, + selectedProfile = null + ).show() + } + + return + } + AuthFollowupType.RE_AUTH, + AuthFollowupType.MISSING_SCOPES, + AuthFollowupType.FULL_AUTH, + -> { + connectionManager.activeConnectionForFeature(QConnection.getInstance())?.let { + reauthConnectionIfNeeded(project, it, isReAuth = true) + } + return + } + else -> { + LOG.warn { "Unknown auth follow up type: $incomingType" } + } + } + } catch (ex: Exception) { + LOG.warn(ex) { "Failed to handle authentication when auth follow up clicked" } + throw ex + } + } + + companion object { + fun getInstance(project: Project) = project.service() + + private val LOG = getLogger() + + fun convertToJsonToSendToChat(command: String, tabId: String, params: String, isPartialResult: Boolean): String = + """ + { + "command":"$command", + "tabId": "$tabId", + "params": $params, + "isPartialResult": $isPartialResult + } + """.trimIndent() + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/FlareUiMessage.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/FlareUiMessage.kt new file mode 100644 index 00000000000..7bc5fe2461f --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/FlareUiMessage.kt @@ -0,0 +1,10 @@ +// 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.flareChat + +data class FlareUiMessage( + val command: String, + val params: Any, + val requestId: String? = null, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt new file mode 100644 index 00000000000..5dfc924005d --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt @@ -0,0 +1,30 @@ +// 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.flareChat + +import com.google.gson.Gson +import com.google.gson.JsonElement +import org.eclipse.lsp4j.ProgressParams + +object ProgressNotificationUtils { + fun getToken(params: ProgressParams): String { + val token = if (params.token.isLeft) { + params.token.left + } else { + params.token.right.toString() + } + + return token + } + + fun getObject(params: ProgressParams, cls: Class?): T? { + val objct = params.value.right as? JsonElement ?: return null + + val gson = Gson() + val element: JsonElement = objct + val obj: T = gson.fromJson(element, cls) + + return obj + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EnumJsonValueAdapter.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EnumJsonValueAdapter.kt new file mode 100644 index 00000000000..a2ad96ec3ea --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EnumJsonValueAdapter.kt @@ -0,0 +1,42 @@ +// 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.model + +import com.fasterxml.jackson.annotation.JsonValue +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken + +/** + * A [Gson] [TypeAdapterFactory] that uses Jackson @[JsonValue] instead of [Enum.name] for de/serialization + */ +class EnumJsonValueAdapter : TypeAdapterFactory { + override fun create( + gson: Gson, + type: TypeToken, + ): TypeAdapter? { + val rawType = type.getRawType() + if (!rawType.isEnum) { + return null + } + + val jsonField = rawType.declaredFields.firstOrNull { it.isAnnotationPresent(JsonValue::class.java) } + ?: return null + + jsonField.isAccessible = true + + return object : TypeAdapter() { + override fun write(out: com.google.gson.stream.JsonWriter, value: T) { + val result = jsonField.get(value) as Any + (gson.getAdapter(result::class.java) as TypeAdapter).write(out, result) + } + + override fun read(`in`: com.google.gson.stream.JsonReader): T { + val jsonValue = `in`.nextString() + return rawType.enumConstants.first { jsonField.get(it).toString() == jsonValue } as T + } + } as TypeAdapter + } +} 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 index 3f2c868c57d..cf961418630 100644 --- 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 @@ -18,12 +18,17 @@ data class AwsMetadata( data class AwsClientCapabilities( val q: DeveloperProfiles, + val window: WindowSettings, ) data class DeveloperProfiles( val developerProfiles: Boolean, ) +data class WindowSettings( + val showSaveFileDialog: Boolean, +) + data class ClientInfoMetadata( val extension: ExtensionMetadata, val clientId: String, @@ -56,6 +61,9 @@ fun createExtendedClientMetadata(project: Project): ExtendedClientMetadata { awsClientCapabilities = AwsClientCapabilities( q = DeveloperProfiles( developerProfiles = true + ), + window = WindowSettings( + showSaveFileDialog = true ) ), contextConfiguration = ContextConfiguration( diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/InlineCompletionStates.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/InlineCompletionStates.kt new file mode 100644 index 00000000000..ac8a73a68d0 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/InlineCompletionStates.kt @@ -0,0 +1,10 @@ +// 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 InlineCompletionStates( + val seen: Boolean, + val accepted: Boolean, + val discarded: Boolean, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LogInlineCompletionSessionResultsParams.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LogInlineCompletionSessionResultsParams.kt new file mode 100644 index 00000000000..d330c12e5ed --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LogInlineCompletionSessionResultsParams.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 + +data class LogInlineCompletionSessionResultsParams( + val sessionId: String, + val completionSessionResult: Map, + val firstCompletionDisplayLatency: Double?, + val totalSessionDisplayTime: Double?, + val typeaheadLength: Long, +) 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 index a0f23875b62..8d8b34cbbb3 100644 --- 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 @@ -14,4 +14,4 @@ data class UpdateConfigurationParams( val settings: LSPAny, ) -typealias LSPAny = Any? +typealias LSPAny = Any diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/AuthFollowUpClicked.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/AuthFollowUpClicked.kt new file mode 100644 index 00000000000..33ab663e0d2 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/AuthFollowUpClicked.kt @@ -0,0 +1,27 @@ +// 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.chat + +import com.fasterxml.jackson.annotation.JsonValue +import com.google.gson.annotations.JsonAdapter +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.EnumJsonValueAdapter + +data class AuthFollowUpClickNotification( + override val command: String, + override val params: AuthFollowUpClickedParams, +) : ChatNotification + +data class AuthFollowUpClickedParams( + val tabId: String, + val messageId: String, + val authFollowupType: AuthFollowupType, +) + +@JsonAdapter(EnumJsonValueAdapter::class) +enum class AuthFollowupType(@JsonValue val repr: String) { + FULL_AUTH("full-auth"), + RE_AUTH("re-auth"), + MISSING_SCOPES("missing_scopes"), + USE_SUPPORTED_AUTH("use-supported-auth"), +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ButtonClick.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ButtonClick.kt new file mode 100644 index 00000000000..7113e762167 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ButtonClick.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.aws.chat + +data class ButtonClickNotification( + override val command: String, + override val params: ButtonClickParams, +) : ChatNotification + +data class ButtonClickParams( + val tabId: String, + val messageId: String, + val buttonId: String, +) + +data class ButtonClickResult( + val success: Boolean, + val failureReason: String?, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt new file mode 100644 index 00000000000..457dedc9ae6 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt @@ -0,0 +1,105 @@ +// 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.chat + +import com.fasterxml.jackson.annotation.JsonValue +import com.google.gson.annotations.JsonAdapter +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.EnumJsonValueAdapter + +data class ChatMessage( + val type: MessageType? = MessageType.ANSWER, + val header: MessageHeader? = null, + val buttons: List