diff --git a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt index 2bccc07914e..c81e78dfe37 100644 --- a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt +++ b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt @@ -59,5 +59,5 @@ fun Project.buildMetadata() = } catch(e: Exception) { logger.warn("Could not determine current commit", e) - "unknownCommit" + "beta.20240910" } diff --git a/gradle.properties b/gradle.properties index 1e7e0e3c7ae..51419b48f45 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Toolkit Version -toolkitVersion=3.30-SNAPSHOT +toolkitVersion=99.99.2 # Publish Settings publishToken= 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 f1df792e004..2ba71479540 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 @@ -27,6 +27,8 @@ + + text="Invoke Amazon Q Inline Suggestions"> + + + + + + + + + + + + + + + , context: DataContext): MutableList { val results = actions.toMutableList() + if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return results + results.sortWith { a, b -> - if (isCodeWhispererPopupAction(a)) { + if (isCodeWhispererForceAction(a)) { + return@sortWith -1 + } else if (isCodeWhispererForceAction(b)) { + return@sortWith 1 + } + + if (a is ChooseItemAction) { return@sortWith -1 - } else if (isCodeWhispererPopupAction(b)) { + } else if (b is ChooseItemAction) { return@sortWith 1 - } else { - 0 } + + if (isCodeWhispererAcceptAction(a)) { + return@sortWith -1 + } else if (isCodeWhispererAcceptAction(b)) { + return@sortWith 1 + } + + 0 } return results } private fun isCodeWhispererAcceptAction(action: AnAction): Boolean = - action is EditorAction && action.handler is CodeWhispererPopupTabHandler + action is CodeWhispererAcceptAction + + private fun isCodeWhispererForceAcceptAction(action: AnAction): Boolean = + action is CodeWhispererForceAcceptAction private fun isCodeWhispererNavigateAction(action: AnAction): Boolean = - action is EditorAction && ( - action.handler is CodeWhispererPopupRightArrowHandler || - action.handler is CodeWhispererPopupLeftArrowHandler - ) + action is CodeWhispererNavigateNextAction || action is CodeWhispererNavigatePrevAction - private fun isCodeWhispererPopupAction(action: AnAction): Boolean = - isCodeWhispererAcceptAction(action) || isCodeWhispererNavigateAction(action) + private fun isCodeWhispererForceAction(action: AnAction): Boolean = + isCodeWhispererForceAcceptAction(action) || isCodeWhispererNavigateAction(action) } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigateNextAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigateNextAction.kt new file mode 100644 index 00000000000..3e091fd145c --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigateNextAction.kt @@ -0,0 +1,32 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.DumbAware +import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService +import software.aws.toolkits.resources.message + +class CodeWhispererNavigateNextAction : AnAction(message("codewhisperer.inline.navigate.next")), DumbAware { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.EDITOR) != null && + CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive() + } + + override fun actionPerformed(e: AnActionEvent) { + val sessionContext = e.project?.getUserData(CodeWhispererService.KEY_SESSION_CONTEXT) ?: return + if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return + ApplicationManager.getApplication().messageBus.syncPublisher( + CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED + ).navigateNext(sessionContext) + } +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigatePrevAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigatePrevAction.kt new file mode 100644 index 00000000000..94dd8ed57e5 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererNavigatePrevAction.kt @@ -0,0 +1,32 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.DumbAware +import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService +import software.aws.toolkits.resources.message + +class CodeWhispererNavigatePrevAction : AnAction(message("codewhisperer.inline.navigate.previous")), DumbAware { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.EDITOR) != null && + CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive() + } + + override fun actionPerformed(e: AnActionEvent) { + val sessionContext = e.project?.getUserData(CodeWhispererService.KEY_SESSION_CONTEXT) ?: return + if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return + ApplicationManager.getApplication().messageBus.syncPublisher( + CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED + ).navigatePrevious(sessionContext) + } +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/ConnectWithAwsToContinueActionWarn.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/ConnectWithAwsToContinueActionWarn.kt deleted file mode 100644 index 0c19cfcff97..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/ConnectWithAwsToContinueActionWarn.kt +++ /dev/null @@ -1,32 +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.actions - -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.project.DumbAwareAction -import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsForCodeWhisperer -import software.aws.toolkits.resources.message - -/** - * Action prompting users to switch to SSO based credential, will nullify accountless credential (delete) - */ -class ConnectWithAwsToContinueActionWarn : DumbAwareAction(message("codewhisperer.notification.accountless.warn.action.connect")) { - override fun actionPerformed(e: AnActionEvent) { - e.project?.let { - runInEdt { - requestCredentialsForCodeWhisperer(it) - } - } - } -} -class ConnectWithAwsToContinueActionError : DumbAwareAction(message("codewhisperer.notification.accountless.error.action.connect")) { - override fun actionPerformed(e: AnActionEvent) { - e.project?.let { - runInEdt { - requestCredentialsForCodeWhisperer(it) - } - } - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/QSwitchToMarketplaceVersionAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/QSwitchToMarketplaceVersionAction.kt new file mode 100644 index 00000000000..52def16c590 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/QSwitchToMarketplaceVersionAction.kt @@ -0,0 +1,94 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.actions + +import com.intellij.icons.AllIcons +import com.intellij.ide.plugins.marketplace.MarketplaceRequests +import com.intellij.notification.NotificationAction +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.updateSettings.impl.PluginDownloader +import com.intellij.openapi.updateSettings.impl.UpdateSettings +import software.aws.toolkits.jetbrains.AwsToolkit +import software.aws.toolkits.jetbrains.core.plugin.PluginUpdateManager +import software.aws.toolkits.jetbrains.utils.notifyInfo +import software.aws.toolkits.resources.AwsCoreBundle +import software.aws.toolkits.resources.message + +class QSwitchToMarketplaceVersionAction : + AnAction( + message("codewhisperer.actions.switch_to_marketplace.title"), + null, + AllIcons.Actions.Refresh + ), + DumbAware { + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.project?.let { + e.presentation.isEnabledAndVisible = PluginUpdateManager.getInstance().isBeta() + } + } + + override fun actionPerformed(e: AnActionEvent) { + UpdateSettings.getInstance().storedPluginHosts.remove(CUSTOM_PLUGIN_URL) + UpdateSettings.getInstance().storedPluginHosts.remove("$CUSTOM_PLUGIN_URL/") + + runInEdt { + ProgressManager.getInstance().run(object : Task.Backgroundable( + null, + message("codewhisperer.actions.switch_to_marketplace.progress.title"), + true + ) { + override fun run(indicator: ProgressIndicator) { + installMarketplaceAwsPlugins(PluginId.getId(AwsToolkit.CORE_PLUGIN_ID), indicator) + installMarketplaceAwsPlugins(PluginId.getId(AwsToolkit.Q_PLUGIN_ID), indicator) + } + }) + } + } + + private fun installMarketplaceAwsPlugins(pluginId: PluginId, indicator: ProgressIndicator) { + // force update to marketplace version + try { + // MarketplaceRequest class is marked as @ApiStatus.Internal + val descriptor = MarketplaceRequests.loadLastCompatiblePluginDescriptors(setOf(pluginId)) + .find { it.pluginId == pluginId } ?: return + + val downloader = PluginDownloader.createDownloader(descriptor) + if (!downloader.prepareToInstall(indicator)) return + downloader.install() + + if (pluginId == PluginId.getId(AwsToolkit.CORE_PLUGIN_ID)) return + notifyInfo( + title = AwsCoreBundle.message("aws.notification.auto_update.title", "Amazon Q"), + content = AwsCoreBundle.message("aws.settings.auto_update.notification.message"), + project = null, + notificationActions = listOf( + NotificationAction.createSimpleExpiring(AwsCoreBundle.message("aws.settings.auto_update.notification.yes")) { + ApplicationManager.getApplication().restart() + }, + NotificationAction.createSimpleExpiring(AwsCoreBundle.message("aws.settings.auto_update.notification.no")) { + } + ) + ) + } catch (e: Exception) { + return + } + return + } + + companion object { + private const val CUSTOM_PLUGIN_URL = "https://d244q0w8umigth.cloudfront.net" + } +} 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 e8694ca0dbc..5b4cfaf3a05 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 @@ -46,6 +46,7 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererCon import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization 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.model.SessionContext 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 @@ -92,6 +93,7 @@ interface CodeWhispererClientAdaptor : Disposable { fun listAvailableCustomizations(): List fun sendUserTriggerDecisionTelemetry( + sessionContext: SessionContext, requestContext: RequestContext, responseContext: ResponseContext, completionType: CodewhispererCompletionType, @@ -293,6 +295,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW } override fun sendUserTriggerDecisionTelemetry( + sessionContext: SessionContext, requestContext: RequestContext, responseContext: ResponseContext, completionType: CodewhispererCompletionType, @@ -303,24 +306,24 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW ): SendTelemetryEventResponse { val fileContext = requestContext.fileContextInfo val programmingLanguage = fileContext.programmingLanguage - var e2eLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency() + 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() - requestContext.latencyContext.codewhispererEndToEndStart + System.nanoTime() - sessionContext.latencyContext.codewhispererEndToEndStart ).toDouble() } return bearerClient().sendTelemetryEvent { requestBuilder -> requestBuilder.telemetryEvent { telemetryEventBuilder -> telemetryEventBuilder.userTriggerDecisionEvent { - it.requestId(requestContext.latencyContext.firstRequestId) + 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(requestContext.latencyContext.paginationFirstCompletionTime) + it.triggerToResponseLatencyMilliseconds(sessionContext.latencyContext.paginationFirstCompletionTime) it.suggestionState(suggestionState.toCodeWhispererSdkType()) it.timestamp(Instant.now()) it.suggestionReferenceCount(suggestionReferenceCount) 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 b7353364c9a..e8db820857c 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 @@ -11,9 +11,9 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager 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.service.CodeWhispererService import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS @@ -23,17 +23,21 @@ import java.util.Stack @Service class CodeWhispererEditorManager { - fun updateEditorWithRecommendation(states: InvocationContext, sessionContext: SessionContext) { - val (requestContext, responseContext, recommendationContext) = states - val (project, editor) = requestContext + fun updateEditorWithRecommendation(sessionContext: SessionContext) { + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + val selectedIndex = sessionContext.selectedIndex + val preview = previews[selectedIndex] + val states = CodeWhispererService.getInstance().getAllPaginationSessions()[preview.jobId] ?: return + val (requestContext, responseContext) = states + val (project, editor) = sessionContext val document = editor.document val primaryCaret = editor.caretModel.primaryCaret - val selectedIndex = sessionContext.selectedIndex - val typeahead = sessionContext.typeahead - val detail = recommendationContext.details[selectedIndex] + val typeahead = preview.typeahead + val detail = preview.detail + val userInput = preview.userInput val reformatted = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( detail, - recommendationContext.userInputSinceInvocation + userInput ) val remainingRecommendation = reformatted.substring(typeahead.length) val originalOffset = primaryCaret.offset - typeahead.length @@ -43,6 +47,8 @@ class CodeWhispererEditorManager { val insertEndOffset = sessionContext.insertEndOffset val endOffsetToReplace = if (insertEndOffset != -1) insertEndOffset else primaryCaret.offset + preview.detail.isAccepted = true + WriteCommandAction.runWriteCommandAction(project) { document.replaceString(originalOffset, endOffsetToReplace, reformatted) PsiDocumentManager.getInstance(project).commitDocument(document) @@ -67,7 +73,7 @@ class CodeWhispererEditorManager { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED, - ).afterAccept(states, sessionContext, rangeMarker) + ).afterAccept(states, previews, sessionContext, rangeMarker) } } } 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 e699e97e856..6fec203cffb 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 @@ -95,7 +95,7 @@ object CodeWhispererEditorUtil { } fun shouldSkipInvokingBasedOnRightContext(editor: Editor): Boolean { - val caretContext = runReadAction { CodeWhispererEditorUtil.extractCaretContext(editor) } + val caretContext = runReadAction { extractCaretContext(editor) } val rightContextLines = caretContext.rightFileContext.split(Regex("\r?\n")) val rightContextCurrentLine = if (rightContextLines.isEmpty()) "" else rightContextLines[0] 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 18845694094..0cc5d929501 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,19 +7,14 @@ import com.intellij.codeInsight.editorActions.TypedHandlerDelegate import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile -import kotlinx.coroutines.Job 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() { - private var triggerOnIdle: Job? = null override fun charTyped(c: Char, project: Project, editor: Editor, psiFiles: PsiFile): Result { - triggerOnIdle?.cancel() - - if (shouldSkipInvokingBasedOnRightContext(editor) - ) { + if (shouldSkipInvokingBasedOnRightContext(editor)) { return Result.CONTINUE } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt index e03b7497260..b9362e41ef9 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt @@ -12,10 +12,12 @@ import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager import software.aws.toolkits.jetbrains.core.credentials.actions.SsoLogoutAction import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection +import software.aws.toolkits.jetbrains.core.plugin.PluginUpdateManager import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererConnectOnGithubAction import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererLearnMoreAction import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererProvideFeedbackAction import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererShowSettingsAction +import software.aws.toolkits.jetbrains.services.codewhisperer.actions.QSwitchToMarketplaceVersionAction import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.ActionProvider import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Customize import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Learn @@ -24,6 +26,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.P import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.PauseCodeScans import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Resume import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.ResumeCodeScans +import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.buildActionListForBeta import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.buildActionListForCodeScan import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.buildActionListForConnectHelp import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.buildActionListForInlineSuggestions @@ -45,6 +48,7 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() { override val sendFeedback = CodeWhispererProvideFeedbackAction() override val connectOnGithub = CodeWhispererConnectOnGithubAction() override val documentation = CodeWhispererLearnMoreAction() + override val switchToMarketplace = QSwitchToMarketplaceVersionAction() } override fun getChildren(e: AnActionEvent?) = e?.project?.let { @@ -65,6 +69,12 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() { add(Separator.create(message("codewhisperer.statusbar.sub_menu.connect_help.title"))) addAll(buildActionListForConnectHelp(actionProvider)) + if (PluginUpdateManager.getInstance().isBeta()) { + add(Separator.create()) + add(Separator.create("Beta")) + addAll(buildActionListForBeta(actionProvider)) + } + add(Separator.create()) add(CodeWhispererShowSettingsAction()) ToolkitConnectionManager.getInstance(it).activeConnectionForFeature(CodeWhispererConnection.getInstance())?.let { c -> diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt index 3b4979572cb..a7dff2e1704 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt @@ -27,6 +27,7 @@ interface ActionProvider { val sendFeedback: T val connectOnGithub: T val documentation: T + val switchToMarketplace: T } @SuppressWarnings("UnusedParameter") @@ -84,3 +85,8 @@ fun buildActionListForConnectHelp(actionProvider: ActionProvider): List buildActionListForBeta(actionProvider: ActionProvider): List = + buildList { + add(actionProvider.switchToMarketplace) + } 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 ce56ea8fc60..6c3bd789c2e 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 @@ -14,15 +14,15 @@ import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info 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.PreviewContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext abstract class CodeWhispererImportAdder { abstract val supportedLanguages: List abstract val dummyFileName: String - fun insertImportStatements(states: InvocationContext, sessionContext: SessionContext) { - val imports = states.recommendationContext.details[sessionContext.selectedIndex] - .recommendation.mostRelevantMissingImports() + fun insertImportStatements(states: InvocationContext, previews: List, sessionContext: SessionContext) { + val imports = previews[sessionContext.selectedIndex].detail.recommendation.mostRelevantMissingImports() LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" } imports.forEach { insertImportStatement(states, it) 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 0a2f8110b5d..a583bd54cde 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 @@ -7,13 +7,14 @@ import com.intellij.openapi.editor.RangeMarker import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +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.popup.CodeWhispererUserActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings object CodeWhispererImportAdderListener : CodeWhispererUserActionListener { internal val LOG = getLogger() - override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) { + override fun afterAccept(states: InvocationContext, previews: List, sessionContext: SessionContext, rangeMarker: RangeMarker) { if (!CodeWhispererSettings.getInstance().isImportAdderEnabled()) { LOG.debug { "Import adder not enabled in user settings" } return @@ -28,6 +29,6 @@ object CodeWhispererImportAdderListener : CodeWhispererUserActionListener { LOG.debug { "No import adder found for $language" } return } - importAdder.insertImportStatements(states, sessionContext) + importAdder.insertImportStatements(states, previews, sessionContext) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt index e72269ce2f7..60daf84c948 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt @@ -6,28 +6,26 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.inlay import com.intellij.idea.AppMode import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorCustomElementRenderer import com.intellij.openapi.editor.Inlay -import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.util.Disposer -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext @Service class CodeWhispererInlayManager { private val existingInlays = mutableListOf>() - fun updateInlays(states: InvocationContext, chunks: List) { - val editor = states.requestContext.editor + fun updateInlays(sessionContext: SessionContext, chunks: List) { clearInlays() chunks.forEach { chunk -> - createCodeWhispererInlays(editor, chunk.inlayOffset, chunk.text, states.popup) + createCodeWhispererInlays(sessionContext, chunk.inlayOffset, chunk.text) } } - private fun createCodeWhispererInlays(editor: Editor, startOffset: Int, inlayText: String, popup: JBPopup) { + private fun createCodeWhispererInlays(sessionContext: SessionContext, startOffset: Int, inlayText: String) { if (inlayText.isEmpty()) return + val editor = sessionContext.editor val firstNewlineIndex = inlayText.indexOf("\n") val firstLine: String val otherLines: String @@ -49,7 +47,7 @@ class CodeWhispererInlayManager { val inlineInlay = editor.inlayModel.addInlineElement(startOffset, true, firstLineRenderer) inlineInlay?.let { existingInlays.add(it) - Disposer.register(popup, it) + Disposer.register(sessionContext, it) } } @@ -73,7 +71,7 @@ class CodeWhispererInlayManager { ) blockInlay?.let { existingInlays.add(it) - Disposer.register(popup, it) + Disposer.register(sessionContext, it) } } } 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 33cf5d77878..bffe7df31ce 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 @@ -4,25 +4,37 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.model import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition 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.codewhisperer.codescan.sessionconfig.PayloadContext import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage +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.CodeWhispererIntelliSenseOnHoverListener +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus +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.CodeWhispererTelemetryService 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( @@ -81,10 +93,19 @@ data class SupplementalContextInfo( } data class RecommendationContext( - val details: List, + val details: MutableList, val userInputOriginal: String, val userInputSinceInvocation: String, - val position: VisualPosition + val position: VisualPosition, + val jobId: Int, + var typeahead: String = "", +) + +data class PreviewContext( + val jobId: Int, + val detail: DetailContext, + val userInput: String, + val typeahead: String, ) data class DetailContext( @@ -95,17 +116,58 @@ data class DetailContext( val isTruncatedOnRight: Boolean, val rightOverlap: String = "", val completionType: CodewhispererCompletionType, + var hasSeen: Boolean = false, + var isAccepted: Boolean = false ) data class SessionContext( - val typeahead: String = "", - val typeaheadOriginal: String = "", - val selectedIndex: Int = 0, + val project: Project, + val editor: Editor, + var popup: JBPopup? = null, + var selectedIndex: Int = -1, val seen: MutableSet = mutableSetOf(), - val isFirstTimeShowingPopup: Boolean = true, + var isFirstTimeShowingPopup: Boolean = true, var toBeRemovedHighlighter: RangeHighlighter? = null, - var insertEndOffset: Int = -1 -) + var insertEndOffset: Int = -1, + var popupDisplayOffset: Int = -1, + val latencyContext: LatencyContext, + var hasAccepted: Boolean = false +) : Disposable { + private var isDisposed = false + init { + project.messageBus.connect().subscribe( + CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER, + object : CodeWhispererIntelliSenseOnHoverListener { + override fun onEnter() { + CodeWhispererPopupManager.getInstance().bringSuggestionInlayToFront(editor, popup, opposite = true) + } + } + ) + } + + @RequiresEdt + override fun dispose() { + CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll( + this, + hasAccepted, + CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) } + ) + setIntelliSensePopupAlpha(editor, 0f) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false) + + if (hasAccepted) { + popup?.closeOk(null) + } else { + popup?.cancel() + } + popup?.let { Disposer.dispose(it) } + popup = null + CodeWhispererInvocationStatus.getInstance().finishInvocation() + isDisposed = true + } + + fun isDisposed() = isDisposed +} data class RecommendationChunk( val text: String, @@ -124,16 +186,24 @@ data class InvocationContext( val requestContext: RequestContext, val responseContext: ResponseContext, val recommendationContext: RecommendationContext, - val popup: JBPopup ) : Disposable { - override fun dispose() {} + private var isDisposed = false + + @RequiresEdt + override fun dispose() { + // TODO: send userTriggerDecision telemetry + + println("state for jobId ${recommendationContext.jobId} is disposed") + isDisposed = true + } + + fun isDisposed() = isDisposed } data class WorkerContext( val requestContext: RequestContext, val responseContext: ResponseContext, val response: GenerateCompletionsResponse, - val popup: JBPopup ) data class CodeScanTelemetryEvent( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt new file mode 100644 index 00000000000..3a46375e250 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt @@ -0,0 +1,22 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.plugin + +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.ide.plugins.PluginManagementPolicy +import com.intellij.ide.plugins.org.PluginManagerFilters + +// Specifically for "Switch Back to Marketplace" action because the default one doesn't support downgrade +class QBetaPluginManagementPolicy : PluginManagementPolicy { + override fun canEnablePlugin(descriptor: IdeaPluginDescriptor?): Boolean = + descriptor?.let { PluginManagerFilters.getInstance().allowInstallingPlugin(it) } ?: true + + override fun canInstallPlugin(descriptor: IdeaPluginDescriptor?): Boolean = canEnablePlugin(descriptor) + + override fun isDowngradeAllowed(localDescriptor: IdeaPluginDescriptor?, remoteDescriptor: IdeaPluginDescriptor?): Boolean = true + + override fun isInstallFromDiskAllowed(): Boolean = PluginManagerFilters.getInstance().allowInstallFromDisk() + + override fun isUpgradeAllowed(localDescriptor: IdeaPluginDescriptor?, remoteDescriptor: IdeaPluginDescriptor?): Boolean = true +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt index fcf2279f478..8faf681c9dd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt @@ -6,11 +6,16 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.idea.AppMode +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionToolbar import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.keymap.MacKeymapUtil +import com.intellij.openapi.util.SystemInfo import com.intellij.ui.IdeBorderFactory import com.intellij.ui.components.ActionLink import com.intellij.util.ui.UIUtil @@ -43,10 +48,38 @@ import javax.swing.JPanel class CodeWhispererPopupComponents { val prevButton = createNavigationButton( - message("codewhisperer.popup.button.prev", POPUP_DIM_HEX) + message( + "codewhisperer.popup.button.prev", + POPUP_DIM_HEX, + run { + // TODO: Doesn't reflect dynamically if users change but didn't restart IDE + val shortcut = ActionManager.getInstance().getAction("codewhisperer.inline.navigate.previous") + .shortcutSet.shortcuts.first() + val keyStroke = (shortcut as KeyboardShortcut).firstKeyStroke + if (SystemInfo.isMac) { + MacKeymapUtil.getKeyStrokeText(keyStroke, " ", true) + } else { + KeymapUtil.getKeystrokeText(keyStroke) + } + } + ) ) val nextButton = createNavigationButton( - message("codewhisperer.popup.button.next", POPUP_DIM_HEX) + message( + "codewhisperer.popup.button.next", + POPUP_DIM_HEX, + run { + // TODO: Doesn't reflect dynamically if users change but didn't restart IDE + val shortcut = ActionManager.getInstance().getAction("codewhisperer.inline.navigate.next") + .shortcutSet.shortcuts.first() + val keyStroke = (shortcut as KeyboardShortcut).firstKeyStroke + if (SystemInfo.isMac) { + MacKeymapUtil.getKeyStrokeText(keyStroke, " ", true) + } else { + KeymapUtil.getKeystrokeText(keyStroke) + } + } + ) ).apply { preferredSize = prevButton.preferredSize } 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 c485c52acbe..8d4e288b5f7 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 @@ -5,30 +5,11 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup import com.intellij.openapi.ui.popup.JBPopupListener import com.intellij.openapi.ui.popup.LightweightWindowEvent -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus -import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService -import java.time.Duration -import java.time.Instant +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService -class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopupListener { - override fun beforeShown(event: LightweightWindowEvent) { - super.beforeShown(event) - CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp() - } +class CodeWhispererPopupListener : JBPopupListener { 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()) } - ) - - CodeWhispererInvocationStatus.getInstance().setPopupActive(false) + CodeWhispererService.getInstance().disposeDisplaySession(event.isOk) } } 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 c5ad37e5271..8fd26b55344 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 @@ -3,14 +3,12 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup -import com.intellij.codeInsight.hint.ParameterInfoController import com.intellij.codeInsight.lookup.LookupManager import com.intellij.idea.AppMode +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_BACKSPACE import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TAB +import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ESCAPE import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.Service @@ -27,37 +25,43 @@ import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.event.EditorMouseEvent +import com.intellij.openapi.editor.event.EditorMouseMotionListener import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key import com.intellij.openapi.wm.WindowManager import com.intellij.ui.ComponentUtil import com.intellij.ui.awt.RelativePoint import com.intellij.ui.popup.AbstractPopup import com.intellij.ui.popup.PopupFactoryImpl +import com.intellij.util.concurrency.annotations.RequiresEdt 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.codewhisperer.actions.CodeWhispererAcceptAction +import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererForceAcceptAction +import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererNavigateNextAction +import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererNavigatePrevAction 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 import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.inlineLabelConstraints 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.PreviewContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererEditorActionHandler import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupBackspaceHandler import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupEnterHandler -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupLeftArrowHandler -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupRightArrowHandler -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupTabHandler +import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupEscHandler import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupTypedHandler import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererAcceptButtonActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererActionListener @@ -65,7 +69,7 @@ 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.service.CodeWhispererService 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 @@ -85,10 +89,6 @@ class CodeWhispererPopupManager { var shouldListenerCancelPopup: Boolean = true private set - var sessionContext = SessionContext() - private set - - private var myPopup: JBPopup? = null init { // Listen for global scheme changes @@ -115,113 +115,86 @@ class CodeWhispererPopupManager { ) } - fun changeStates( - states: InvocationContext, - indexChange: Int, - typeaheadChange: String, - typeaheadAdded: Boolean, - recommendationAdded: Boolean = false - ) { - val (_, _, recommendationContext, popup) = states - val (details) = recommendationContext - if (recommendationAdded) { - LOG.debug { - "Add recommendations to the existing CodeWhisperer session, current number of recommendations: ${details.size}" - } - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED) - .recommendationAdded(states, sessionContext) - return - } - val typeaheadOriginal = - if (typeaheadAdded) { - sessionContext.typeaheadOriginal + typeaheadChange - } else { - if (typeaheadChange.length > sessionContext.typeaheadOriginal.length) { - cancelPopup(popup) - return - } - sessionContext.typeaheadOriginal.substring( - 0, - sessionContext.typeaheadOriginal.length - typeaheadChange.length - ) - } - val isReverse = indexChange < 0 - val userInput = states.recommendationContext.userInputSinceInvocation - val validCount = getValidCount(details, userInput, typeaheadOriginal) - val validSelectedIndex = getValidSelectedIndex(details, userInput, sessionContext.selectedIndex, typeaheadOriginal) + @RequiresEdt + fun changeStatesForNavigation(sessionContext: SessionContext, indexChange: Int) { + val validCount = getValidCount() + val validSelectedIndex = getValidSelectedIndex(sessionContext.selectedIndex) if ((validSelectedIndex == validCount - 1 && indexChange == 1) || (validSelectedIndex == 0 && indexChange == -1) ) { return } - val selectedIndex = findNewSelectedIndex( - isReverse, - details, - userInput, - sessionContext.selectedIndex + indexChange, - typeaheadOriginal + val isReverse = indexChange < 0 + val selectedIndex = findNewSelectedIndex(isReverse, sessionContext.selectedIndex + indexChange) + + sessionContext.selectedIndex = selectedIndex + sessionContext.isFirstTimeShowingPopup = false + + ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged( + sessionContext ) - if (selectedIndex == -1 || !isValidRecommendation(details[selectedIndex], userInput, typeaheadOriginal)) { - LOG.debug { "None of the recommendation is valid at this point, cancelling the popup" } - cancelPopup(popup) + } + + @RequiresEdt + fun changeStatesForTypeahead( + sessionContext: SessionContext, + typeaheadChange: String, + typeaheadAdded: Boolean + ) { + CodeWhispererService.getInstance().updateTypeahead(typeaheadChange, typeaheadAdded) + updateSessionSelectedIndex(sessionContext) + sessionContext.isFirstTimeShowingPopup = false + + ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged( + sessionContext + ) + } + + @RequiresEdt + fun changeStatesForShowing(sessionContext: SessionContext, states: InvocationContext, recommendationAdded: Boolean = false) { + sessionContext.isFirstTimeShowingPopup = !recommendationAdded + if (recommendationAdded) { + ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED) + .recommendationAdded(states, sessionContext) return } - val typeahead = resolveTypeahead(states, selectedIndex, typeaheadOriginal) - val isFirstTimeShowingPopup = indexChange == 0 && typeaheadChange.isEmpty() - sessionContext = SessionContext( - typeahead, - typeaheadOriginal, - selectedIndex, - sessionContext.seen, - isFirstTimeShowingPopup, - sessionContext.toBeRemovedHighlighter - ) + + updateSessionSelectedIndex(sessionContext) + if (sessionContext.popupDisplayOffset == -1) { + sessionContext.popupDisplayOffset = sessionContext.editor.caretModel.offset + } ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged( - states, sessionContext ) } - private fun resolveTypeahead(states: InvocationContext, selectedIndex: Int, typeahead: String): String { - val recommendation = states.recommendationContext.details[selectedIndex].reformatted.content() - val userInput = states.recommendationContext.userInputSinceInvocation - var indexOfFirstNonWhiteSpace = typeahead.indexOfFirst { !it.isWhitespace() } - if (indexOfFirstNonWhiteSpace == -1) { - indexOfFirstNonWhiteSpace = typeahead.length + private fun updateSessionSelectedIndex(sessionContext: SessionContext) { + val selectedIndex = findNewSelectedIndex(false, sessionContext.selectedIndex) + if (selectedIndex == -1) { + LOG.debug { "None of the recommendation is valid at this point, cancelling the popup" } + CodeWhispererService.getInstance().disposeDisplaySession(false) + return } - for (i in 0..indexOfFirstNonWhiteSpace) { - val subTypeahead = typeahead.substring(i) - if (recommendation.startsWith(userInput + subTypeahead)) return subTypeahead - } - return typeahead + sessionContext.selectedIndex = selectedIndex } - fun updatePopupPanel(states: InvocationContext, sessionContext: SessionContext) { - val userInput = states.recommendationContext.userInputSinceInvocation - val details = states.recommendationContext.details + fun updatePopupPanel(sessionContext: SessionContext?) { + if (sessionContext == null || sessionContext.selectedIndex == -1 || sessionContext.isDisposed()) return val selectedIndex = sessionContext.selectedIndex - val typeaheadOriginal = sessionContext.typeaheadOriginal - val validCount = getValidCount(details, userInput, typeaheadOriginal) - val validSelectedIndex = getValidSelectedIndex(details, userInput, selectedIndex, typeaheadOriginal) + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + if (selectedIndex >= previews.size) return + val validCount = getValidCount() + val validSelectedIndex = getValidSelectedIndex(selectedIndex) updateSelectedRecommendationLabelText(validSelectedIndex, validCount) updateNavigationPanel(validSelectedIndex, validCount) - updateImportPanel(details[selectedIndex].recommendation.mostRelevantMissingImports()) - updateCodeReferencePanel(states.requestContext.project, details[selectedIndex].recommendation.references()) + updateImportPanel(previews[selectedIndex].detail.recommendation.mostRelevantMissingImports()) + updateCodeReferencePanel(sessionContext.project, previews[selectedIndex].detail.recommendation.references()) } - fun render( - states: InvocationContext, - sessionContext: SessionContext, - overlappingLinesCount: Int, - isRecommendationAdded: Boolean, - isScrolling: Boolean - ) { - updatePopupPanel(states, sessionContext) - - val caretPoint = states.requestContext.editor.offsetToXY(states.requestContext.caretPosition.offset) - sessionContext.seen.add(sessionContext.selectedIndex) + fun render(sessionContext: SessionContext, isRecommendationAdded: Boolean, isScrolling: Boolean) { + updatePopupPanel(sessionContext) // There are four cases that render() is called: // 1. Popup showing for the first time, both booleans are false, we should show the popup and update the latency @@ -232,20 +205,11 @@ class CodeWhispererPopupManager { // emit any events. // 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 (!isRecommendationAdded) { - showPopup(states, sessionContext, states.popup, caretPoint, overlappingLinesCount) - if (!isScrolling) { - states.requestContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime() - states.requestContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime() - } - } - if (isScrolling || - CodeWhispererInvocationStatus.getInstance().hasExistingInvocation() || - !sessionContext.isFirstTimeShowingPopup - ) { - return - } - CodeWhispererTelemetryService.getInstance().sendClientComponentLatencyEvent(states) + if (isRecommendationAdded) return + showPopup(sessionContext) + if (isScrolling) return + sessionContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime() + sessionContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime() } fun dontClosePopupAndRun(runnable: () -> Unit) { @@ -257,84 +221,36 @@ class CodeWhispererPopupManager { } } - fun reset() { - sessionContext = SessionContext() - } - - fun cancelPopup(popup: JBPopup) { - popup.cancel() - Disposer.dispose(popup) - } - - fun closePopup(popup: JBPopup) { - popup.closeOk(null) - Disposer.dispose(popup) - } - - fun closePopup() { - myPopup?.let { - it.closeOk(null) - Disposer.dispose(it) + fun showPopup(sessionContext: SessionContext, force: Boolean = false) { + val p = sessionContext.editor.offsetToXY(sessionContext.popupDisplayOffset) + val popup: JBPopup? + if (sessionContext.popup == null) { + popup = initPopup() + sessionContext.popup = popup + CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp() + initPopupListener(sessionContext, popup) + } else { + popup = sessionContext.popup } - } - - fun showPopup( - states: InvocationContext, - sessionContext: SessionContext, - popup: JBPopup, - p: Point, - overlappingLinesCount: Int - ) { - val editor = states.requestContext.editor - val detailContexts = states.recommendationContext.details - val userInputOriginal = states.recommendationContext.userInputOriginal - val userInput = states.recommendationContext.userInputSinceInvocation - val selectedIndex = sessionContext.selectedIndex - val typeaheadOriginal = sessionContext.typeaheadOriginal - val typeahead = sessionContext.typeahead + val editor = sessionContext.editor + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + val userInputOriginal = previews[sessionContext.selectedIndex].userInput val userInputLines = userInputOriginal.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 - val yBelowLastLine = p.y + (lineCount + additionalLines + userInputLines - overlappingLinesCount) * editor.lineHeight - val yAboveFirstLine = p.y - popupSize.height + (additionalLines + userInputLines) * editor.lineHeight + val yAboveFirstLine = p.y - popupSize.height + userInputLines * editor.lineHeight + val popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height) val editorRect = editor.scrollingModel.visibleArea - var popupRect = Rectangle(p.x, yBelowLastLine, popupSize.width, popupSize.height) var shouldHidePopup = false - CodeWhispererInvocationStatus.getInstance().setPopupActive(true) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) - // Check if the current editor still has focus. If not, don't show the popup. - val isSameEditorAsTrigger = if (!AppMode.isRemoteDevHost()) { - editor.contentComponent.isFocusOwner - } else { - FileEditorManager.getInstance(states.requestContext.project).selectedTextEditorWithRemotes.firstOrNull() == editor - } - if (!isSameEditorAsTrigger) { - LOG.debug { "Current editor no longer has focus, not showing the popup" } - cancelPopup(popup) - return + if (!editorRect.contains(popupRect)) { + // popup location above first line don't work, so don't show the popup + shouldHidePopup = true } - val popupLocation = - if (!editorRect.contains(popupRect)) { - popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height) - if (!editorRect.contains(popupRect)) { - // both popup location (below last line and above first line) don't work, so don't show the popup - shouldHidePopup = true - } - LOG.debug { - "Show popup above the first line of recommendation. " + - "Editor position: $editorRect, popup position: $popupRect" - } - Point(p.x, yAboveFirstLine) - } else { - LOG.debug { - "Show popup below the last line of recommendation. " + - "Editor position: $editorRect, popup position: $popupRect" - } - Point(p.x, yBelowLastLine) - } + // popup to always display above the current editing line + val popupLocation = Point(p.x, yAboveFirstLine) val relativePopupLocationToEditor = RelativePoint(editor.contentComponent, popupLocation) @@ -347,8 +263,11 @@ class CodeWhispererPopupManager { } } else { if (!AppMode.isRemoteDevHost()) { - popup.show(relativePopupLocationToEditor) + if (force && !shouldHidePopup) { + popup.show(relativePopupLocationToEditor) + } } else { + // TODO: Fix in remote case the popup should display above the current editing line // TODO: For now, the popup will always display below the suggestions, without checking // if the location the popup is about to show at stays in the editor window or not, due to // the limitation of BackendBeAbstractPopup @@ -363,22 +282,20 @@ class CodeWhispererPopupManager { editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, popupPositionForRemote) popup.showInBestPositionFor(editor) } - val perceivedLatency = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged() - CodeWhispererTelemetryService.getInstance().sendPerceivedLatencyEvent( - detailContexts[selectedIndex].requestId, - states.requestContext, - states.responseContext, - perceivedLatency - ) } - // popup.popupWindow is null in remote host - if (!AppMode.isRemoteDevHost()) { - if (shouldHidePopup) { - WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 1f) - } else { - WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 0.1f) - } + bringSuggestionInlayToFront(editor, popup, !force) + } + + fun bringSuggestionInlayToFront(editor: Editor, popup: JBPopup?, opposite: Boolean = false) { + val qInlinePopupAlpha = if (opposite) 1f else 0.1f + val intelliSensePopupAlpha = if (opposite) 0f else 0.8f + + (popup as AbstractPopup?)?.popupWindow?.let { + WindowManager.getInstance().setAlphaModeRatio(it, qInlinePopupAlpha) + } + ComponentUtil.getWindow(LookupManager.getActiveLookup(editor)?.component)?.let { + WindowManager.getInstance().setAlphaModeRatio(it, intelliSensePopupAlpha) } } @@ -386,166 +303,196 @@ class CodeWhispererPopupManager { .createComponentPopupBuilder(popupComponents.panel, null) .setAlpha(0.1F) .setCancelOnClickOutside(true) - .setCancelOnOtherWindowOpen(true) - .setCancelKeyEnabled(true) .setCancelOnWindowDeactivation(true) - .createPopup().also { - myPopup = it - } + .createPopup() fun getReformattedRecommendation(detailContext: DetailContext, userInput: String) = detailContext.reformatted.content().substring(userInput.length) - fun initPopupListener(states: InvocationContext) { - addPopupListener(states) - states.requestContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListener(states), states) - addButtonActionListeners(states) - addMessageSubscribers(states) - setPopupActionHandlers(states) - addComponentListeners(states) + private fun initPopupListener(sessionContext: SessionContext, popup: JBPopup) { + addPopupListener(popup) + sessionContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListener(sessionContext), sessionContext) + addButtonActionListeners(sessionContext) + addMessageSubscribers(sessionContext) + setPopupActionHandlers(sessionContext) + addComponentListeners(sessionContext) } - private fun addPopupListener(states: InvocationContext) { - val listener = CodeWhispererPopupListener(states) - states.popup.addListener(listener) - Disposer.register(states) { states.popup.removeListener(listener) } + private fun addPopupListener(popup: JBPopup) { + val listener = CodeWhispererPopupListener() + popup.addListener(listener) + Disposer.register(popup) { + popup.removeListener(listener) + } } - private fun addMessageSubscribers(states: InvocationContext) { - val connect = ApplicationManager.getApplication().messageBus.connect(states) + private fun addMessageSubscribers(sessionContext: SessionContext) { + val connect = ApplicationManager.getApplication().messageBus.connect(sessionContext) connect.subscribe( CODEWHISPERER_USER_ACTION_PERFORMED, object : CodeWhispererUserActionListener { - override fun navigateNext(states: InvocationContext) { - changeStates(states, 1, "", true) + override fun navigateNext(sessionContext: SessionContext) { + changeStatesForNavigation(sessionContext, 1) } - override fun navigatePrevious(states: InvocationContext) { - changeStates(states, -1, "", true) + override fun navigatePrevious(sessionContext: SessionContext) { + changeStatesForNavigation(sessionContext, -1) } - override fun backspace(states: InvocationContext, diff: String) { - changeStates(states, 0, diff, false) + override fun backspace(sessionContext: SessionContext, diff: String) { + changeStatesForTypeahead(sessionContext, diff, false) } - override fun enter(states: InvocationContext, diff: String) { - changeStates(states, 0, diff, true) + override fun enter(sessionContext: SessionContext, diff: String) { + changeStatesForTypeahead(sessionContext, diff, true) } - override fun type(states: InvocationContext, diff: String) { + override fun type(sessionContext: SessionContext, diff: String) { // remove the character at primaryCaret if it's the same as the typed character - val caretOffset = states.requestContext.editor.caretModel.primaryCaret.offset - val document = states.requestContext.editor.document + val caretOffset = sessionContext.editor.caretModel.primaryCaret.offset + val document = sessionContext.editor.document val text = document.charsSequence if (caretOffset < text.length && diff == text[caretOffset].toString()) { - WriteCommandAction.runWriteCommandAction(states.requestContext.project) { + WriteCommandAction.runWriteCommandAction(sessionContext.project) { document.deleteString(caretOffset, caretOffset + 1) } } - changeStates(states, 0, diff, true) + changeStatesForTypeahead(sessionContext, diff, true) } - override fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) { + override fun beforeAccept(sessionContext: SessionContext) { dontClosePopupAndRun { - CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(states, sessionContext) + CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(sessionContext) } - closePopup(states.popup) + CodeWhispererService.getInstance().disposeDisplaySession(true) } } ) } - private fun addButtonActionListeners(states: InvocationContext) { - popupComponents.prevButton.addButtonActionListener(CodeWhispererPrevButtonActionListener(states)) - popupComponents.nextButton.addButtonActionListener(CodeWhispererNextButtonActionListener(states)) - popupComponents.acceptButton.addButtonActionListener(CodeWhispererAcceptButtonActionListener(states)) + private fun addButtonActionListeners(sessionContext: SessionContext) { + popupComponents.prevButton.addButtonActionListener(CodeWhispererPrevButtonActionListener(sessionContext), sessionContext) + popupComponents.nextButton.addButtonActionListener(CodeWhispererNextButtonActionListener(sessionContext), sessionContext) + popupComponents.acceptButton.addButtonActionListener(CodeWhispererAcceptButtonActionListener(sessionContext), sessionContext) } - private fun JButton.addButtonActionListener(listener: CodeWhispererActionListener) { + private fun JButton.addButtonActionListener(listener: CodeWhispererActionListener, sessionContext: SessionContext) { this.addActionListener(listener) - Disposer.register(listener.states) { this.removeActionListener(listener) } + Disposer.register(sessionContext) { this.removeActionListener(listener) } } - private fun setPopupActionHandlers(states: InvocationContext) { + private fun setPopupActionHandlers(sessionContext: SessionContext) { val actionManager = EditorActionManager.getInstance() - setPopupTypedHandler(CodeWhispererPopupTypedHandler(TypedAction.getInstance().rawHandler, states)) - setPopupActionHandler(ACTION_EDITOR_TAB, CodeWhispererPopupTabHandler(states)) - setPopupActionHandler(ACTION_EDITOR_MOVE_CARET_LEFT, CodeWhispererPopupLeftArrowHandler(states)) - setPopupActionHandler(ACTION_EDITOR_MOVE_CARET_RIGHT, CodeWhispererPopupRightArrowHandler(states)) + + sessionContext.project.putUserData(CodeWhispererService.KEY_SESSION_CONTEXT, sessionContext) + + setPopupTypedHandler(CodeWhispererPopupTypedHandler(TypedAction.getInstance().rawHandler, sessionContext), sessionContext) + setPopupActionHandler(ACTION_EDITOR_ESCAPE, CodeWhispererPopupEscHandler(sessionContext), sessionContext) setPopupActionHandler( ACTION_EDITOR_ENTER, - CodeWhispererPopupEnterHandler(actionManager.getActionHandler(ACTION_EDITOR_ENTER), states) + CodeWhispererPopupEnterHandler(actionManager.getActionHandler(ACTION_EDITOR_ENTER), sessionContext), + sessionContext ) setPopupActionHandler( ACTION_EDITOR_BACKSPACE, - CodeWhispererPopupBackspaceHandler(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), states) + CodeWhispererPopupBackspaceHandler(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), sessionContext), + sessionContext ) } - private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandler) { + private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandler, sessionContext: SessionContext) { val oldTypedHandler = TypedAction.getInstance().setupRawHandler(newHandler) - Disposer.register(newHandler.states) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) } + Disposer.register(sessionContext) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) } } - private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandler) { + private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandler, sessionContext: SessionContext) { val oldHandler = EditorActionManager.getInstance().setActionHandler(id, newHandler) - Disposer.register(newHandler.states) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) } + Disposer.register(sessionContext) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) } } - private fun addComponentListeners(states: InvocationContext) { - val editor = states.requestContext.editor - val codewhispererSelectionListener: SelectionListener = object : SelectionListener { + private fun addComponentListeners(sessionContext: SessionContext) { + val editor = sessionContext.editor + val codeWhispererSelectionListener: SelectionListener = object : SelectionListener { override fun selectionChanged(event: SelectionEvent) { if (shouldListenerCancelPopup) { - cancelPopup(states.popup) + CodeWhispererService.getInstance().disposeDisplaySession(false) } super.selectionChanged(event) } } - editor.selectionModel.addSelectionListener(codewhispererSelectionListener) - Disposer.register(states) { editor.selectionModel.removeSelectionListener(codewhispererSelectionListener) } + editor.selectionModel.addSelectionListener(codeWhispererSelectionListener) + Disposer.register(sessionContext) { editor.selectionModel.removeSelectionListener(codeWhispererSelectionListener) } - val codewhispererDocumentListener: DocumentListener = object : DocumentListener { + val codeWhispererDocumentListener: DocumentListener = object : DocumentListener { override fun documentChanged(event: DocumentEvent) { if (shouldListenerCancelPopup) { - cancelPopup(states.popup) + // handle IntelliSense accept case + // TODO: handle bulk delete (delete word) case + if (editor.document == event.document && + editor.caretModel.offset == event.offset && + event.newLength > event.oldLength + ) { + dontClosePopupAndRun { + super.documentChanged(event) + editor.caretModel.moveCaretRelatively(event.newLength, 0, false, false, true) + changeStatesForTypeahead(sessionContext, event.newFragment.toString(), true) + } + return + } else { + CodeWhispererService.getInstance().disposeDisplaySession(false) + } } super.documentChanged(event) } } - editor.document.addDocumentListener(codewhispererDocumentListener, states) + editor.document.addDocumentListener(codeWhispererDocumentListener, sessionContext) - val codewhispererCaretListener: CaretListener = object : CaretListener { + val codeWhispererCaretListener: CaretListener = object : CaretListener { override fun caretPositionChanged(event: CaretEvent) { if (shouldListenerCancelPopup) { - cancelPopup(states.popup) + CodeWhispererService.getInstance().disposeDisplaySession(false) } super.caretPositionChanged(event) } } - editor.caretModel.addCaretListener(codewhispererCaretListener) - Disposer.register(states) { editor.caretModel.removeCaretListener(codewhispererCaretListener) } + editor.caretModel.addCaretListener(codeWhispererCaretListener) + Disposer.register(sessionContext) { editor.caretModel.removeCaretListener(codeWhispererCaretListener) } val editorComponent = editor.contentComponent if (editorComponent.isShowing) { val window = ComponentUtil.getWindow(editorComponent) val windowListener: ComponentListener = object : ComponentAdapter() { - override fun componentMoved(event: ComponentEvent) { - cancelPopup(states.popup) + override fun componentMoved(e: ComponentEvent) { + CodeWhispererService.getInstance().disposeDisplaySession(false) + super.componentMoved(e) } override fun componentShown(e: ComponentEvent?) { - cancelPopup(states.popup) + CodeWhispererService.getInstance().disposeDisplaySession(false) super.componentShown(e) } } window?.addComponentListener(windowListener) - Disposer.register(states) { window?.removeComponentListener(windowListener) } + Disposer.register(sessionContext) { window?.removeComponentListener(windowListener) } } + + val suggestionHoverEnterListener: EditorMouseMotionListener = object : EditorMouseMotionListener { + override fun mouseMoved(e: EditorMouseEvent) { + if (e.inlay != null) { + showPopup(sessionContext, force = true) + } else { + sessionContext.project.messageBus.syncPublisher( + CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER, + ).onEnter() + } + super.mouseMoved(e) + } + } + editor.addEditorMouseMotionListener(suggestionHoverEnterListener, sessionContext) } private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) { - if (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) { + if (CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation()) { popupComponents.recommendationInfoLabel.text = message("codewhisperer.popup.pagination_info") LOG.debug { "Pagination in progress. Current total: $validCount" } } else { @@ -618,18 +565,10 @@ class CodeWhispererPopupManager { } } - fun hasConflictingPopups(editor: Editor): Boolean = - ParameterInfoController.existsWithVisibleHintForEditor(editor, true) || - LookupManager.getActiveLookup(editor) != null - - private fun findNewSelectedIndex( - isReverse: Boolean, - detailContexts: List, - userInput: String, - start: Int, - typeahead: String - ): Int { - val count = detailContexts.size + fun findNewSelectedIndex(isReverse: Boolean, selectedIndex: Int): Int { + val start = if (selectedIndex == -1) 0 else selectedIndex + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + val count = previews.size val unit = if (isReverse) -1 else 1 var currIndex: Int for (i in 0 until count) { @@ -637,45 +576,34 @@ class CodeWhispererPopupManager { if (currIndex < 0) { currIndex += count } - if (isValidRecommendation(detailContexts[currIndex], userInput, typeahead)) { + if (isValidRecommendation(previews[currIndex])) { return currIndex } } return -1 } - private fun getValidCount(detailContexts: List, userInput: String, typeahead: String): Int = - detailContexts.filter { isValidRecommendation(it, userInput, typeahead) }.size - - private fun getValidSelectedIndex( - detailContexts: List, - userInput: String, - selectedIndex: Int, - typeahead: String - ): Int { - var currIndexIgnoreInvalid = 0 - detailContexts.forEachIndexed { index, value -> + private fun getValidCount(): Int = + CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo().filter { isValidRecommendation(it) }.size + + private fun getValidSelectedIndex(selectedIndex: Int): Int { + var curr = 0 + + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + previews.forEachIndexed { index, triple -> if (index == selectedIndex) { - return currIndexIgnoreInvalid + return curr } - if (isValidRecommendation(value, userInput, typeahead)) { - currIndexIgnoreInvalid++ + if (isValidRecommendation(triple)) { + curr++ } } return -1 } - private fun isValidRecommendation(detailContext: DetailContext, userInput: String, typeahead: String): Boolean { - if (detailContext.isDiscarded) return false - if (detailContext.recommendation.content().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 - } - return false + private fun isValidRecommendation(preview: PreviewContext): Boolean { + if (preview.detail.isDiscarded) return false + return preview.detail.recommendation.content().startsWith(preview.userInput + preview.typeahead) } companion object { @@ -693,17 +621,17 @@ class CodeWhispererPopupManager { } interface CodeWhispererPopupStateChangeListener { - fun stateChanged(states: InvocationContext, sessionContext: SessionContext) {} - fun scrolled(states: InvocationContext, sessionContext: SessionContext) {} + fun stateChanged(sessionContext: SessionContext) {} + fun scrolled(sessionContext: SessionContext) {} fun recommendationAdded(states: InvocationContext, sessionContext: SessionContext) {} } interface CodeWhispererUserActionListener { - fun backspace(states: InvocationContext, diff: String) {} - fun enter(states: InvocationContext, diff: String) {} - fun type(states: InvocationContext, diff: String) {} - fun navigatePrevious(states: InvocationContext) {} - fun navigateNext(states: InvocationContext) {} - fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) {} - fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {} + fun backspace(sessionContext: SessionContext, diff: String) {} + fun enter(sessionContext: SessionContext, diff: String) {} + fun type(sessionContext: SessionContext, diff: String) {} + fun navigatePrevious(sessionContext: SessionContext) {} + fun navigateNext(sessionContext: SessionContext) {} + fun beforeAccept(sessionContext: SessionContext) {} + fun afterAccept(states: InvocationContext, previews: List, sessionContext: SessionContext, rangeMarker: RangeMarker) {} } 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 e4bb87feec8..e5dce240995 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 @@ -15,22 +15,26 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationCo import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { - override fun stateChanged(states: InvocationContext, sessionContext: SessionContext) { - val editor = states.requestContext.editor + override fun stateChanged(sessionContext: SessionContext) { + val editor = sessionContext.editor val editorManager = CodeWhispererEditorManager.getInstance() + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() val selectedIndex = sessionContext.selectedIndex - val typeahead = sessionContext.typeahead - val detail = states.recommendationContext.details[selectedIndex] + val typeahead = previews[selectedIndex].typeahead + val detail = previews[selectedIndex].detail val caretOffset = editor.caretModel.primaryCaret.offset 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 + previews[selectedIndex].userInput, ).substring(typeahead.length) val remainingLines = remaining.split("\n") @@ -61,7 +65,7 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { }, HighlighterTargetArea.EXACT_RANGE ) - Disposer.register(states.popup) { + Disposer.register(sessionContext) { editor.markupModel.removeHighlighter(rangeHighlighter) } sessionContext.toBeRemovedHighlighter = rangeHighlighter @@ -87,57 +91,21 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { // inlay chunks are chunks from first line(chunks) and an additional chunk from other lines val inlayChunks = chunks + listOf(RecommendationChunk(otherLinesInlayText, 0, chunks.last().inlayOffset)) - CodeWhispererInlayManager.getInstance().updateInlays(states, inlayChunks) + CodeWhispererInlayManager.getInstance().updateInlays(sessionContext, inlayChunks) CodeWhispererPopupManager.getInstance().render( - states, sessionContext, - overlappingLinesCount, isRecommendationAdded = false, isScrolling = false ) } - override fun scrolled(states: InvocationContext, sessionContext: SessionContext) { - if (states.popup.isDisposed) return - val editor = states.requestContext.editor - val editorManager = CodeWhispererEditorManager.getInstance() - val selectedIndex = sessionContext.selectedIndex - val typeahead = sessionContext.typeahead - val detail = states.recommendationContext.details[selectedIndex] - - // get matching brackets from recommendations to the brackets after caret position - val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( - detail, - states.recommendationContext.userInputSinceInvocation - ).substring(typeahead.length) - - val remainingLines = remaining.split("\n") - val otherLinesOfRemaining = remainingLines.drop(1) - - // process other lines inlays, where we do tail-head matching as much as possible - val overlappingLinesCount = editorManager.findOverLappingLines( - editor, - otherLinesOfRemaining, - detail.isTruncatedOnRight, - sessionContext - ) - - CodeWhispererPopupManager.getInstance().render( - states, - sessionContext, - overlappingLinesCount, - isRecommendationAdded = false, - isScrolling = true - ) + override fun scrolled(sessionContext: SessionContext) { + sessionContext.isFirstTimeShowingPopup = false + CodeWhispererPopupManager.getInstance().render(sessionContext, isRecommendationAdded = false, isScrolling = true) } override fun recommendationAdded(states: InvocationContext, sessionContext: SessionContext) { - CodeWhispererPopupManager.getInstance().render( - states, - sessionContext, - 0, - isRecommendationAdded = true, - isScrolling = false - ) + sessionContext.isFirstTimeShowingPopup = false + CodeWhispererPopupManager.getInstance().render(sessionContext, isRecommendationAdded = true, isScrolling = false) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererEditorActionHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererEditorActionHandler.kt index 0e02fb260bf..a2cea3c836f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererEditorActionHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererEditorActionHandler.kt @@ -4,6 +4,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext -abstract class CodeWhispererEditorActionHandler(val states: InvocationContext) : EditorActionHandler() +abstract class CodeWhispererEditorActionHandler(val sessionContext: SessionContext) : EditorActionHandler() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupBackspaceHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupBackspaceHandler.kt index 8f32eceac34..cdd510a31eb 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupBackspaceHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupBackspaceHandler.kt @@ -8,15 +8,16 @@ import com.intellij.openapi.application.ApplicationManager 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.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager class CodeWhispererPopupBackspaceHandler( private val defaultHandler: EditorActionHandler, - states: InvocationContext -) : CodeWhispererEditorActionHandler(states) { + sessionContext: SessionContext +) : CodeWhispererEditorActionHandler(sessionContext) { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { val popupManager = CodeWhispererPopupManager.getInstance() + popupManager.dontClosePopupAndRun { val oldOffset = editor.caretModel.offset defaultHandler.execute(editor, caret, dataContext) @@ -24,7 +25,7 @@ class CodeWhispererPopupBackspaceHandler( val newText = "a".repeat(oldOffset - newOffset) ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).backspace(states, newText) + ).backspace(sessionContext, newText) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEnterHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEnterHandler.kt index 1c71f0675d6..836185a23b5 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEnterHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEnterHandler.kt @@ -9,13 +9,13 @@ import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.util.TextRange -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 class CodeWhispererPopupEnterHandler( private val defaultHandler: EditorActionHandler, - states: InvocationContext -) : CodeWhispererEditorActionHandler(states) { + sessionContext: SessionContext +) : CodeWhispererEditorActionHandler(sessionContext) { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { val popupManager = CodeWhispererPopupManager.getInstance() popupManager.dontClosePopupAndRun { @@ -25,7 +25,7 @@ class CodeWhispererPopupEnterHandler( val newText = editor.document.getText(TextRange.create(oldOffset, newOffset)) ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).enter(states, newText) + ).enter(sessionContext, newText) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEscHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEscHandler.kt new file mode 100644 index 00000000000..634f76e885a --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupEscHandler.kt @@ -0,0 +1,16 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService + +class CodeWhispererPopupEscHandler(sessionContext: SessionContext) : CodeWhispererEditorActionHandler(sessionContext) { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + CodeWhispererService.getInstance().disposeDisplaySession(false) + } +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupLeftArrowHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupLeftArrowHandler.kt deleted file mode 100644 index 020e1434808..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupLeftArrowHandler.kt +++ /dev/null @@ -1,19 +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.popup.handlers - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager - -class CodeWhispererPopupLeftArrowHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - ApplicationManager.getApplication().messageBus.syncPublisher( - CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).navigatePrevious(states) - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupRightArrowHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupRightArrowHandler.kt deleted file mode 100644 index 9efba65a80b..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupRightArrowHandler.kt +++ /dev/null @@ -1,19 +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.popup.handlers - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager - -class CodeWhispererPopupRightArrowHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - ApplicationManager.getApplication().messageBus.syncPublisher( - CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).navigateNext(states) - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTabHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTabHandler.kt deleted file mode 100644 index c92eae91062..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTabHandler.kt +++ /dev/null @@ -1,19 +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.popup.handlers - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager - -class CodeWhispererPopupTabHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - ApplicationManager.getApplication().messageBus.syncPublisher( - CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext) - } -} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTypedHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTypedHandler.kt index 7e18feaf3e0..501409dc4ce 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTypedHandler.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTypedHandler.kt @@ -7,19 +7,19 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.TypedActionHandler -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 class CodeWhispererPopupTypedHandler( private val defaultHandler: TypedActionHandler, - val states: InvocationContext, + val sessionContext: SessionContext ) : TypedActionHandler { override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) { CodeWhispererPopupManager.getInstance().dontClosePopupAndRun { defaultHandler.execute(editor, charTyped, dataContext) ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).type(states, charTyped.toString()) + ).type(sessionContext, charTyped.toString()) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt index 7bc77295a99..6a7a03e132e 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt @@ -4,14 +4,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners import com.intellij.openapi.application.ApplicationManager -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 java.awt.event.ActionEvent -class CodeWhispererAcceptButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) { +class CodeWhispererAcceptButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) { override fun actionPerformed(e: ActionEvent?) { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext) + ).beforeAccept(sessionContext) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererActionListener.kt index c04f8cc444c..23f0975e66e 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererActionListener.kt @@ -3,7 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import java.awt.event.ActionListener -abstract class CodeWhispererActionListener(val states: InvocationContext) : ActionListener +abstract class CodeWhispererActionListener(val sessionContext: SessionContext) : ActionListener diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererNextButtonActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererNextButtonActionListener.kt index ce1d34432ee..d11f219a330 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererNextButtonActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererNextButtonActionListener.kt @@ -4,14 +4,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners import com.intellij.openapi.application.ApplicationManager -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 java.awt.event.ActionEvent -class CodeWhispererNextButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) { +class CodeWhispererNextButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) { override fun actionPerformed(e: ActionEvent?) { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).navigateNext(states) + ).navigateNext(sessionContext) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPrevButtonActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPrevButtonActionListener.kt index e77fdf469b5..273a40a8e16 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPrevButtonActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPrevButtonActionListener.kt @@ -4,14 +4,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners import com.intellij.openapi.application.ApplicationManager -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 java.awt.event.ActionEvent -class CodeWhispererPrevButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) { +class CodeWhispererPrevButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) { override fun actionPerformed(e: ActionEvent?) { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).navigatePrevious(states) + ).navigatePrevious(sessionContext) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererScrollListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererScrollListener.kt index f1dfab068a8..4ebea981628 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererScrollListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererScrollListener.kt @@ -6,20 +6,20 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.event.VisibleAreaEvent import com.intellij.openapi.editor.event.VisibleAreaListener -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.service.CodeWhispererInvocationStatus -class CodeWhispererScrollListener(private val states: InvocationContext) : VisibleAreaListener { +class CodeWhispererScrollListener(private val sessionContext: SessionContext) : VisibleAreaListener { override fun visibleAreaChanged(e: VisibleAreaEvent) { val oldRect = e.oldRectangle val newRect = e.newRectangle - if (CodeWhispererInvocationStatus.getInstance().isPopupActive() && + if (CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive() && (oldRect.x != newRect.x || oldRect.y != newRect.y) ) { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_POPUP_STATE_CHANGED - ).scrolled(states, CodeWhispererPopupManager.getInstance().sessionContext) + ).scrolled(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 68c578d092e..d1dcbcdf1cd 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 @@ -4,7 +4,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service @@ -26,6 +25,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmi 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.util.CodeWhispererConstants +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.INVOCATION_DELAY import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState import software.aws.toolkits.telemetry.CodewhispererTriggerType @@ -42,6 +42,8 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa private var lastInvocationTime: Instant? = null private var lastInvocationLineNum: Int? = null + private var lastCharTypedTime: Instant? = null + private var lastTrigger: Job? = null init { scheduleReset() @@ -55,9 +57,6 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa fun tryInvokeAutoTrigger(editor: Editor, triggerType: CodeWhispererAutomatedTriggerType): Job? { // only needed for Classifier group, thus calculate it lazily val classifierResult: ClassifierResult by lazy { shouldTriggerClassifier(editor, triggerType.telemetryType) } - val language = runReadAction { - FileDocumentManager.getInstance().getFile(editor.document)?.programmingLanguage() - } ?: CodeWhispererUnknownLanguage.INSTANCE // we need classifier result for any type of triggering for classifier group for supported languages triggerType.calculationResult = classifierResult.calculatedResult @@ -84,8 +83,12 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa if (!CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger)) { return null } + if (hasEnoughDelaySinceLastTrigger()) { + lastTrigger?.cancel() + } lastInvocationTime = Instant.now() + lastCharTypedTime = Instant.now() lastInvocationLineNum = runReadAction { editor.caretModel.visualPosition.line } val latencyContext = LatencyContext().apply { @@ -95,32 +98,23 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa val coroutineScope = applicationCoroutineScope() - return when (triggerType) { - is CodeWhispererAutomatedTriggerType.IdleTime -> run { - coroutineScope.launch { - // TODO: potential race condition between hasExistingInvocation and entering edt - // but in that case we will just return in performAutomatedTriggerAction - while (!CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToInvokeCodeWhisperer() || - CodeWhispererInvocationStatus.getInstance().hasExistingInvocation() - ) { - if (!isActive) return@launch - delay(CodeWhispererConstants.IDLE_TIME_CHECK_INTERVAL) - } - runInEdt { - if (CodeWhispererInvocationStatus.getInstance().isPopupActive()) return@runInEdt - performAutomatedTriggerAction(editor, CodeWhispererAutomatedTriggerType.IdleTime(), latencyContext) - } + return run { + coroutineScope.launch(EDT) { + while (!hasEnoughDelaySinceLastTrigger()) { + if (!isActive) return@launch + delay(CodeWhispererConstants.IDLE_TIME_CHECK_INTERVAL) } - } - else -> run { - coroutineScope.launch(EDT) { - performAutomatedTriggerAction(editor, triggerType, latencyContext) - } + performAutomatedTriggerAction(editor, triggerType, latencyContext) + }.also { + lastTrigger = it } } } + private fun hasEnoughDelaySinceLastTrigger(): Boolean = + lastCharTypedTime == null || lastCharTypedTime?.plusMillis(INVOCATION_DELAY)?.isBefore(Instant.now()) == true + private fun scheduleReset() { if (!alarm.isDisposed) { alarm.addRequest({ resetPreviousStates() }, Duration.ofSeconds(120).toMillis()) 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 2a2d70bd903..5bf5852bcc8 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 @@ -16,9 +16,8 @@ import java.util.concurrent.atomic.AtomicBoolean @Service class CodeWhispererInvocationStatus { - private val isInvokingCodeWhisperer: AtomicBoolean = AtomicBoolean(false) + private val isInvokingService: AtomicBoolean = AtomicBoolean(false) private var invokingSessionId: String? = null - private var timeAtLastInvocationComplete: Instant? = null var timeAtLastDocumentChanged: Instant = Instant.now() private set private var isPopupActive: Boolean = false @@ -26,30 +25,22 @@ class CodeWhispererInvocationStatus { var popupStartTimestamp: Instant? = null private set - fun checkExistingInvocationAndSet(): Boolean = - if (isInvokingCodeWhisperer.getAndSet(true)) { - LOG.debug { "Have existing CodeWhisperer invocation, sessionId: $invokingSessionId" } - true - } else { - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(true) - LOG.debug { "Starting CodeWhisperer invocation" } - false - } + fun startInvocation() { + isInvokingService.set(true) + ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(true) + LOG.debug { "Starting CodeWhisperer invocation" } + } - fun hasExistingInvocation(): Boolean = isInvokingCodeWhisperer.get() + fun hasExistingServiceInvocation(): Boolean = isInvokingService.get() fun finishInvocation() { - if (isInvokingCodeWhisperer.compareAndSet(true, false)) { + if (isInvokingService.compareAndSet(true, false)) { ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(false) LOG.debug { "Ending CodeWhisperer invocation" } invokingSessionId = null } } - fun setInvocationComplete() { - timeAtLastInvocationComplete = Instant.now() - } - fun documentChanged() { timeAtLastDocumentChanged = Instant.now() } @@ -69,9 +60,9 @@ class CodeWhispererInvocationStatus { return timeCanShowCodeWhisperer.isBefore(Instant.now()) } - fun isPopupActive(): Boolean = isPopupActive + fun isDisplaySessionActive(): Boolean = isPopupActive - fun setPopupActive(value: Boolean) { + fun setDisplaySessionActive(value: Boolean) { isPopupActive = value } @@ -84,11 +75,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 3f6305c1f7d..fb574f47c17 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 @@ -9,6 +9,7 @@ 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.codewhisperer.model.DetailContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType import kotlin.math.max @@ -63,7 +64,7 @@ class CodeWhispererRecommendationManager { userInput: String, recommendations: List, requestId: String, - ): List { + ): MutableList { val seen = mutableSetOf() return recommendations.map { val isDiscardedByUserInput = !it.content().startsWith(userInput) || it.content() == userInput @@ -126,7 +127,7 @@ class CodeWhispererRecommendationManager { overlap, getCompletionType(it) ) - } + }.toMutableList() } fun findRightContextOverlap( 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 13af5360d41..ed44be831b6 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 @@ -6,6 +6,7 @@ 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.actionSystem.DataKey import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt @@ -15,16 +16,18 @@ import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic +import io.ktor.utils.io.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 @@ -46,7 +49,6 @@ 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.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 @@ -64,6 +66,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContex 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.PreviewContext 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 @@ -94,6 +97,11 @@ import java.util.concurrent.TimeUnit class CodeWhispererService(private val cs: CoroutineScope) : Disposable { private val codeInsightSettingsFacade = CodeInsightsSettingsFacade() private var refreshFailure: Int = 0 + private val ongoingRequests = mutableMapOf() + val ongoingRequestsContext = mutableMapOf() + private var jobId = 0 + private var sessionContext: SessionContext? = null + var isBetaExpired: Boolean = false init { Disposer.register(this, codeInsightSettingsFacade) @@ -148,6 +156,11 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } } + if (isBetaExpired && triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { + showCodeWhispererInfoHint(editor, "Current beta period ended, please switch to the marketplace version") + return + } + latencyContext.credentialFetchingEnd = System.nanoTime() val psiFile = runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(editor.document) } @@ -161,18 +174,27 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val isInjectedFile = runReadAction { psiFile.isInjectedText() } if (isInjectedFile) return + val currentJobId = jobId++ val requestContext = try { - getRequestContext(triggerTypeInfo, editor, project, psiFile, latencyContext) + getRequestContext(triggerTypeInfo, editor, project, psiFile) } catch (e: Exception) { LOG.debug { e.message.toString() } CodeWhispererTelemetryService.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) return } + val caretContext = requestContext.fileContextInfo.caretContext + ongoingRequestsContext.forEach { (k, v) -> + val vCaretContext = v.fileContextInfo.caretContext + if (vCaretContext == caretContext) { + LOG.debug { "same caretContext found from job: $k, left context ${vCaretContext.leftContextOnCurrentLine}, jobId: $currentJobId" } + return + } + } val language = requestContext.fileContextInfo.programmingLanguage val leftContext = requestContext.fileContextInfo.caretContext.leftFileContext if (!language.isCodeCompletionSupported() || (checkLeftContextKeywordsForJsonAndYaml(leftContext, language.languageId))) { - LOG.debug { "Programming language $language is not supported by CodeWhisperer" } + LOG.debug { "Programming language $language is not supported by CodeWhisperer, jobId: $currentJobId" } if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { showCodeWhispererInfoHint( requestContext.editor, @@ -183,7 +205,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } LOG.debug { - "Calling CodeWhisperer service, trigger type: ${triggerTypeInfo.triggerType}" + + "Calling CodeWhisperer service, jobId: $currentJobId, trigger type: ${triggerTypeInfo.triggerType}" + if (triggerTypeInfo.triggerType == CodewhispererTriggerType.AutoTrigger) { ", auto-trigger type: ${triggerTypeInfo.automatedTriggerType}" } else { @@ -191,28 +213,34 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } } - val invocationStatus = CodeWhispererInvocationStatus.getInstance() - if (invocationStatus.checkExistingInvocationAndSet()) { - return - } + CodeWhispererInvocationStatus.getInstance().startInvocation() - invokeCodeWhispererInBackground(requestContext) + invokeCodeWhispererInBackground(requestContext, currentJobId, latencyContext) } - internal fun invokeCodeWhispererInBackground(requestContext: RequestContext): Job { - val popup = CodeWhispererPopupManager.getInstance().initPopup() - Disposer.register(popup) { CodeWhispererInvocationStatus.getInstance().finishInvocation() } + internal fun invokeCodeWhispererInBackground(requestContext: RequestContext, currentJobId: Int, latencyContext: LatencyContext): Job? { + ongoingRequestsContext[currentJobId] = requestContext + val sessionContext = sessionContext ?: SessionContext(requestContext.project, requestContext.editor, latencyContext = latencyContext) + + // In rare cases when there's an ongoing session and subsequent triggers are from a different project or editor -- + // we will cancel the existing session(since we've already moved to a different project or editor simply return. + if (requestContext.project != sessionContext.project || requestContext.editor != sessionContext.editor) { + disposeDisplaySession(false) + return null + } + this.sessionContext = sessionContext 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) + val coroutineScope = projectCoroutineScope(requestContext.project) - var states: InvocationContext? = null var lastRecommendationIndex = -1 + val line = requestContext.fileContextInfo.caretContext.leftContextOnCurrentLine + println("triggering, last char $line, jobId: $currentJobId") val job = coroutineScope.launch { try { val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( @@ -224,8 +252,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { ) var startTime = System.nanoTime() - requestContext.latencyContext.codewhispererPreprocessingEnd = System.nanoTime() - requestContext.latencyContext.paginationAllCompletionsStart = System.nanoTime() + latencyContext.codewhispererPreprocessingEnd = System.nanoTime() + latencyContext.paginationAllCompletionsStart = System.nanoTime() CodeWhispererInvocationStatus.getInstance().setInvocationStart() var requestCount = 0 for (response in responseIterable) { @@ -236,13 +264,13 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { 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 = latency - requestContext.latencyContext.firstRequestId = requestId + latencyContext.codewhispererPostprocessingStart = System.nanoTime() + latencyContext.paginationFirstCompletionTime = latency + latencyContext.firstRequestId = requestId CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId) } if (response.nextToken().isEmpty()) { - requestContext.latencyContext.paginationAllCompletionsEnd = System.nanoTime() + latencyContext.paginationAllCompletionsEnd = System.nanoTime() } val responseContext = ResponseContext(sessionId) logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null) @@ -250,6 +278,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) .onSuccess(requestContext.fileContextInfo) CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( + currentJobId, requestId, requestContext, responseContext, @@ -271,11 +300,11 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { // 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) + val workerContext = WorkerContext(requestContext, responseContext, validatedResponse) if (workerContexts.isNotEmpty()) { workerContexts.add(workerContext) } else { - if (states == null && !popup.isDisposed && + if (ongoingRequests.values.filterNotNull().isEmpty() && !CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer() ) { // It's the first response, and no enough delay before showing @@ -285,7 +314,16 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } runInEdt { workerContexts.forEach { - states = processCodeWhispererUI(it, states) + processCodeWhispererUI( + sessionContext, + it, + ongoingRequests[currentJobId], + coroutineScope, + currentJobId + ) + if (!ongoingRequests.contains(currentJobId)) { + coroutineScope.coroutineContext.cancel() + } } workerContexts.clear() } @@ -293,14 +331,25 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { workerContexts.add(workerContext) } else { // Have enough delay before showing for the first response, or it's subsequent responses - states = processCodeWhispererUI(workerContext, states) + processCodeWhispererUI( + sessionContext, + workerContext, + ongoingRequests[currentJobId], + coroutineScope, + currentJobId + ) } } } if (!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 CodeWhisperer session exit" } + LOG.debug { "Skipping sending remaining requests on inactive CodeWhisperer session exit" } + return@launch + } + if (requestCount >= PAGINATION_REQUEST_COUNT_ALLOWED) { + LOG.debug { "Only $PAGINATION_REQUEST_COUNT_ALLOWED request per pagination session for now" } + CodeWhispererInvocationStatus.getInstance().finishInvocation() break } } @@ -321,6 +370,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val responseContext = ResponseContext(sessionId) CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( + currentJobId, requestId, requestContext, responseContext, @@ -350,7 +400,6 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { ) ) CodeWhispererInvocationStatus.getInstance().finishInvocation() - CodeWhispererInvocationStatus.getInstance().setInvocationComplete() requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) } @@ -358,7 +407,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { showRecommendationsInPopup( requestContext.editor, requestContext.triggerTypeInfo, - requestContext.latencyContext + latencyContext ) } return@launch @@ -366,7 +415,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { 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") - } else if (e is software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException) { + } 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") @@ -389,6 +438,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId) logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType) CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( + currentJobId, requestId, requestContext, responseContext, @@ -410,7 +460,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { // We should only show error hint when CodeWhisperer popup is not visible, // and make it silent if CodeWhisperer popup is showing. runInEdt { - if (!CodeWhispererInvocationStatus.getInstance().isPopupActive()) { + if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) { showCodeWhispererErrorHint(requestContext.editor, displayMessage) } } @@ -418,15 +468,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } CodeWhispererInvocationStatus.getInstance().finishInvocation() runInEdt { - states?.let { - CodeWhispererPopupManager.getInstance().updatePopupPanel( - it, - CodeWhispererPopupManager.getInstance().sessionContext - ) - } + CodeWhispererPopupManager.getInstance().updatePopupPanel(sessionContext) } - } finally { - CodeWhispererInvocationStatus.getInstance().setInvocationComplete() } } @@ -434,61 +477,66 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } @RequiresEdt - private fun processCodeWhispererUI(workerContext: WorkerContext, currStates: InvocationContext?): InvocationContext? { + private fun processCodeWhispererUI( + sessionContext: SessionContext, + workerContext: WorkerContext, + currStates: InvocationContext?, + coroutine: CoroutineScope, + jobId: Int + ) { val requestContext = workerContext.requestContext val responseContext = workerContext.responseContext val response = workerContext.response - 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" } - return null + if (!coroutine.isActive || sessionContext.isDisposed()) { + LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. RequestId: $requestId, jobId: $jobId" } + return } if (requestContext.editor.isDisposed) { - LOG.debug { "Stop showing CodeWhisperer recommendations since editor is disposed. RequestId: $requestId" } - CodeWhispererPopupManager.getInstance().cancelPopup(popup) - return null + LOG.debug { "Stop showing all CodeWhisperer recommendations since editor is disposed. RequestId: $requestId, jobId: $jobId" } + disposeDisplaySession(false) + return } - if (response.nextToken().isEmpty()) { - CodeWhispererInvocationStatus.getInstance().finishInvocation() - } + CodeWhispererInvocationStatus.getInstance().finishInvocation() val caretMovement = CodeWhispererEditorManager.getInstance().getCaretMovement( requestContext.editor, requestContext.caretPosition ) - val isPopupShowing: Boolean + val isPopupShowing = checkRecommendationsValidity(currStates, false) val nextStates: InvocationContext? if (currStates == null) { - // first response - nextStates = initStates(requestContext, responseContext, response, caretMovement, popup) - isPopupShowing = false + // first response for the jobId + nextStates = initStates(jobId, requestContext, responseContext, response, caretMovement, coroutine) - // 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 + // receiving a null state means caret has moved backward, + // so we are going to cancel the current job if (nextStates == null) { - LOG.debug { "Cancelling popup and exiting CodeWhisperer session. RequestId: $requestId" } - CodeWhispererPopupManager.getInstance().cancelPopup(popup) - return null + return } } else { - // subsequent responses + // subsequent responses for the jobId nextStates = updateStates(currStates, response) - isPopupShowing = checkRecommendationsValidity(currStates, false) } + LOG.debug { "Adding ${response.completions().size} completions to the session. RequestId: $requestId, jobId: $jobId" } - val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, response.nextToken().isEmpty()) + // TODO: may have bug when it's a mix of auto-trigger + manual trigger + val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, true) + val allSuggestions = ongoingRequests.values.filterNotNull().flatMap { it.recommendationContext.details } + val valid = allSuggestions.filter { !it.isDiscarded }.size + LOG.debug { "Suggestions status: valid: $valid, discarded: ${allSuggestions.size - valid}" } // 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()) { + // TODO: may have bug; visit later + if (nextStates.recommendationContext.details.isEmpty()) { LOG.debug { "Received just an empty list from this session, requestId: $requestId" } CodeWhispererTelemetryService.getInstance().sendUserDecisionEvent( requestContext, @@ -508,38 +556,42 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { ) } if (!hasAtLeastOneValid) { - if (response.nextToken().isEmpty()) { - LOG.debug { "None of the recommendations are valid, exiting CodeWhisperer session" } - CodeWhispererPopupManager.getInstance().cancelPopup(popup) - return null + LOG.debug { "None of the recommendations are valid, exiting current CodeWhisperer pagination session" } + // If there's only one ongoing request, after disposing this, the entire session will also end + if (ongoingRequests.keys.size == 1) { + disposeDisplaySession(false) + } else { + disposeJob(jobId) + sessionContext.selectedIndex = CodeWhispererPopupManager.getInstance().findNewSelectedIndex(true, sessionContext.selectedIndex) } } else { - updateCodeWhisperer(nextStates, isPopupShowing) + updateCodeWhisperer(sessionContext, nextStates, isPopupShowing) } - return nextStates } private fun initStates( + jobId: Int, requestContext: RequestContext, responseContext: ResponseContext, response: GenerateCompletionsResponse, caretMovement: CaretMovement, - popup: JBPopup + coroutine: CoroutineScope, ): 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 and exiting the session. Request ID: $requestId, jobId: $jobId" } + val detailContexts = recommendations.map { + DetailContext("", it, it, true, false, "", getCompletionType(it)) + }.toMutableList() + val recommendationContext = RecommendationContext(detailContexts, "", "", VisualPosition(0, 0), jobId) + ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext, coroutine) + disposeDisplaySession(false) return null } + val userInputOriginal = CodeWhispererEditorManager.getInstance().getUserInputSinceInvocation( requestContext.editor, requestContext.caretPosition.offset @@ -563,8 +615,9 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { recommendations, requestId ) - val recommendationContext = RecommendationContext(detailContexts, userInputOriginal, userInput, visualPosition) - return buildInvocationContext(requestContext, responseContext, recommendationContext, popup) + val recommendationContext = RecommendationContext(detailContexts, userInputOriginal, userInput, visualPosition, jobId) + ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext, coroutine) + return ongoingRequests[jobId] } private fun updateStates( @@ -572,24 +625,19 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { response: GenerateCompletionsResponse ): InvocationContext { val recommendationContext = states.recommendationContext - val details = recommendationContext.details val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( states.requestContext, recommendationContext.userInputSinceInvocation, response.completions(), response.responseMetadata().requestId() ) - Disposer.dispose(states) - val updatedStates = states.copy( - recommendationContext = recommendationContext.copy(details = details + newDetailContexts) - ) - Disposer.register(states.popup, updatedStates) - CodeWhispererPopupManager.getInstance().initPopupListener(updatedStates) - return updatedStates + recommendationContext.details.addAll(newDetailContexts) + return states } - private fun checkRecommendationsValidity(states: InvocationContext, showHint: Boolean): Boolean { + private fun checkRecommendationsValidity(states: InvocationContext?, showHint: Boolean): Boolean { + if (states == null) return false val details = states.recommendationContext.details // set to true when at least one is not discarded or empty @@ -604,35 +652,69 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { return hasAtLeastOneValid } - private fun updateCodeWhisperer(states: InvocationContext, recommendationAdded: Boolean) { - CodeWhispererPopupManager.getInstance().changeStates(states, 0, "", true, recommendationAdded) + private fun updateCodeWhisperer(sessionContext: SessionContext, states: InvocationContext, recommendationAdded: Boolean) { + CodeWhispererPopupManager.getInstance().changeStatesForShowing(sessionContext, states, recommendationAdded) } - private fun sendDiscardedUserDecisionEventForAll( - requestContext: RequestContext, - responseContext: ResponseContext, - recommendations: List - ) { - val detailContexts = recommendations.map { - DetailContext("", it, it, true, false, "", getCompletionType(it)) + @RequiresEdt + private fun disposeJob(jobId: Int) { + ongoingRequests[jobId]?.let { Disposer.dispose(it) } + ongoingRequests.remove(jobId) + ongoingRequestsContext.remove(jobId) + } + + @RequiresEdt + fun disposeDisplaySession(accept: Boolean) { + // avoid duplicate session disposal logic + if (sessionContext == null || sessionContext?.isDisposed() == true) return + + sessionContext?.let { + it.hasAccepted = accept + Disposer.dispose(it) } - val recommendationContext = RecommendationContext(detailContexts, "", "", VisualPosition(0, 0)) + sessionContext = null + val jobIds = ongoingRequests.keys.toList() + jobIds.forEach { jobId -> disposeJob(jobId) } + ongoingRequests.clear() + ongoingRequestsContext.clear() + } - CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll( - requestContext, - responseContext, - recommendationContext, - SessionContext(), - false - ) + fun getAllSuggestionsPreviewInfo() = + ongoingRequests.values.filterNotNull().flatMap { element -> + val context = element.recommendationContext + context.details.map { + PreviewContext(context.jobId, it, context.userInputSinceInvocation, context.typeahead) + } + } + + fun getAllPaginationSessions() = ongoingRequests + + fun updateTypeahead(typeaheadChange: String, typeaheadAdded: Boolean) { + val recommendations = ongoingRequests.values.filterNotNull() + recommendations.forEach { + val newTypeahead = + if (typeaheadAdded) { + it.recommendationContext.typeahead + typeaheadChange + } else { + if (typeaheadChange.length > it.recommendationContext.typeahead.length) { + LOG.debug { "Typeahead change is longer than the current typeahead, exiting the session" } + disposeDisplaySession(false) + return + } + it.recommendationContext.typeahead.substring( + 0, + it.recommendationContext.typeahead.length - typeaheadChange.length + ) + } + it.recommendationContext.typeahead = newTypeahead + } } fun getRequestContext( triggerTypeInfo: TriggerTypeInfo, editor: Editor, project: Project, - psiFile: PsiFile, - latencyContext: LatencyContext + psiFile: PsiFile ): RequestContext { // 1. file context val fileContext: FileContextInfo = runReadAction { FileContextProvider.getInstance(project).extractFileContext(editor, psiFile) } @@ -657,7 +739,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { // 5. customization val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn - return RequestContext(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, latencyContext, customizationArn) + return RequestContext(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, customizationArn) } fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse { @@ -683,26 +765,18 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { requestContext: RequestContext, responseContext: ResponseContext, recommendationContext: RecommendationContext, - popup: JBPopup + coroutine: CoroutineScope ): InvocationContext { - addPopupChildDisposables(popup) // Creating a disposable for managing all listeners lifecycle attached to the popup. // previously(before pagination) we use popup as the parent disposable. // After pagination, listeners need to be updated as states are updated, for the same popup, // so disposable chain becomes popup -> disposable -> listeners updates, and disposable gets replaced on every // state update. - val states = InvocationContext(requestContext, responseContext, recommendationContext, popup) - Disposer.register(popup, states) - CodeWhispererPopupManager.getInstance().initPopupListener(states) - return states - } - - private fun addPopupChildDisposables(popup: JBPopup) { - codeInsightSettingsFacade.disableCodeInsightUntil(popup) - - Disposer.register(popup) { - CodeWhispererPopupManager.getInstance().reset() + val states = InvocationContext(requestContext, responseContext, recommendationContext) + Disposer.register(states) { + coroutine.cancel(CancellationException("Cancelling the current coroutine when the pagination session context is disposed")) } + return states } private fun logServiceInvocation( @@ -742,13 +816,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { return false } - if (CodeWhispererPopupManager.getInstance().hasConflictingPopups(editor)) { - LOG.debug { "Find other active popup windows before triggering CodeWhisperer, not invoking service" } - return false - } - - if (CodeWhispererInvocationStatus.getInstance().isPopupActive()) { - LOG.debug { "Find an existing CodeWhisperer popup window before triggering CodeWhisperer, not invoking service" } + if (CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) { + LOG.debug { "Find an existing CodeWhisperer session before triggering CodeWhisperer, not invoking service" } return false } return true @@ -767,11 +836,17 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { companion object { private val LOG = getLogger() private const val MAX_REFRESH_ATTEMPT = 3 + private const val PAGINATION_REQUEST_COUNT_ALLOWED = 1 val CODEWHISPERER_CODE_COMPLETION_PERFORMED: Topic = Topic.create( "CodeWhisperer code completion service invoked", CodeWhispererCodeCompletionServiceListener::class.java ) + val CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER: Topic = Topic.create( + "CodeWhisperer intelliSense popup on hover", + CodeWhispererIntelliSenseOnHoverListener::class.java + ) + val KEY_SESSION_CONTEXT = Key.create("codewhisperer.session") fun getInstance(): CodeWhispererService = service() const val KET_SESSION_ID = "x-amzn-SessionId" @@ -828,7 +903,6 @@ data class RequestContext( val fileContextInfo: FileContextInfo, private val supplementalContextDeferred: Deferred, val connection: ToolkitConnection?, - val latencyContext: LatencyContext, val customizationArn: String? ) { // TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only @@ -842,7 +916,6 @@ data class RequestContext( null } } - else -> field } @@ -859,3 +932,7 @@ data class ResponseContext( interface CodeWhispererCodeCompletionServiceListener { fun onSuccess(fileContextInfo: FileContextInfo) {} } + +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 9db80018184..8e10475fe0a 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 @@ -4,13 +4,17 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.settings import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager import com.intellij.openapi.options.BoundConfigurable +import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.SearchableConfigurable +import com.intellij.openapi.options.ex.Settings import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.components.ActionLink import com.intellij.ui.dsl.builder.bindIntText import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.EdtExecutorService import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType @@ -18,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhisp import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled import software.aws.toolkits.resources.message import java.awt.Font +import java.util.concurrent.TimeUnit // As the connection is project-level, we need to make this project-level too (we have different config for Sono vs SSO users) class CodeWhispererConfigurable(private val project: Project) : @@ -87,6 +92,20 @@ class CodeWhispererConfigurable(private val project: Project) : bindSelected(codeWhispererSettings::isImportAdderEnabled, codeWhispererSettings::toggleImportAdder) }.comment(message("aws.settings.codewhisperer.automatic_import_adder.tooltip")) } + + row { + link("Configure inline suggestion keybindings") { e -> + val settings = DataManager.getInstance().getDataContext(e.source as ActionLink).getData(Settings.KEY) ?: return@link + val configurable: Configurable = settings.find("preferences.keymap") ?: return@link + + settings.select(configurable, Q_INLINE_KEYBINDING_SEARCH_TEXT) + + // workaround for certain cases for sometimes the string is not input there + EdtExecutorService.getScheduledExecutorInstance().schedule({ + settings.select(configurable, Q_INLINE_KEYBINDING_SEARCH_TEXT) + }, 500, TimeUnit.MILLISECONDS) + } + } } group(message("aws.settings.codewhisperer.group.q_chat")) { @@ -109,7 +128,7 @@ class CodeWhispererConfigurable(private val project: Project) : intTextField( range = IntRange(0, 50) ).bindIntText(codeWhispererSettings::getProjectContextIndexThreadCount, codeWhispererSettings::setProjectContextIndexThreadCount) - .align(AlignX.FILL).apply { + .apply { connect.subscribe( ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { @@ -119,14 +138,14 @@ class CodeWhispererConfigurable(private val project: Project) : } ) enabled(invoke) - }.comment(message("aws.settings.codewhisperer.project_context_index_thread.tooltip")) + }.comment(message("aws.settings.codewhisperer.project_context_index_thread.tooltip"), maxLineLength = 47) } row(message("aws.settings.codewhisperer.project_context_index_max_size")) { intTextField( range = IntRange(1, 250) ).bindIntText(codeWhispererSettings::getProjectContextIndexMaxSize, codeWhispererSettings::setProjectContextIndexMaxSize) - .align(AlignX.FILL).apply { + .apply { connect.subscribe( ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { @@ -183,4 +202,8 @@ class CodeWhispererConfigurable(private val project: Project) : } } } + + companion object { + private const val Q_INLINE_KEYBINDING_SEARCH_TEXT = "inline suggestion" + } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt index 749e5e273d0..24105ace86f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt @@ -94,6 +94,13 @@ class CodeWhispererSettings : PersistentStateComponent, sessionContext: SessionContext, rangeMarker: RangeMarker) { if (states.requestContext.fileContextInfo.programmingLanguage != language) return rangeMarkers.add(rangeMarker) val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return 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 3cbe44fffd8..a8565978b5d 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 @@ -14,6 +14,7 @@ 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 @@ -22,13 +23,13 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWh 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.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.CodeWhispererFeatureConfigService import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus +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.settings.CodeWhispererSettings @@ -90,6 +91,7 @@ class CodeWhispererTelemetryService { } fun sendServiceInvocationEvent( + jobId: Int, requestId: String, requestContext: RequestContext, responseContext: ResponseContext, @@ -98,6 +100,7 @@ class CodeWhispererTelemetryService { latency: Double, exceptionType: String? ) { + LOG.debug { "Sending serviceInvocation for $requestId, jobId: $jobId" } val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo val (offset, line) = requestContext.caretPosition @@ -186,6 +189,7 @@ class CodeWhispererTelemetryService { } fun sendUserTriggerDecisionEvent( + sessionContext: SessionContext, requestContext: RequestContext, responseContext: ResponseContext, recommendationContext: RecommendationContext, @@ -222,6 +226,7 @@ class CodeWhispererTelemetryService { try { val response = CodeWhispererClientAdaptor.getInstance(project) .sendUserTriggerDecisionTelemetry( + sessionContext, requestContext, responseContext, completionType, @@ -245,7 +250,7 @@ class CodeWhispererTelemetryService { CodewhispererTelemetry.userTriggerDecision( project = project, codewhispererSessionId = responseContext.sessionId, - codewhispererFirstRequestId = requestContext.latencyContext.firstRequestId, + codewhispererFirstRequestId = sessionContext.latencyContext.firstRequestId, credentialStartUrl = getConnectionStartUrl(requestContext.connection), codewhispererIsPartialAcceptance = null, codewhispererPartialAcceptanceCount = null, @@ -264,7 +269,7 @@ class CodeWhispererTelemetryService { codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(), codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(), codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision, - codewhispererTimeToFirstRecommendation = requestContext.latencyContext.paginationFirstCompletionTime, + codewhispererTimeToFirstRecommendation = sessionContext.latencyContext.paginationFirstCompletionTime, codewhispererPreviousSuggestionState = previousUserTriggerDecision, codewhispererSuggestionState = suggestionState, codewhispererClassifierResult = classifierResult, @@ -385,59 +390,60 @@ class CodeWhispererTelemetryService { } 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) + CodeWhispererService.getInstance().getAllPaginationSessions().forEach { (jobId, state) -> + if (state == null) return@forEach + val decisions = mutableListOf() + val details = state.recommendationContext.details - decisions.add(suggestionState) - } + details.forEachIndexed { index, detail -> + val suggestionState = recordSuggestionState(detail, hasUserAccepted) + sendUserDecisionEvent(state.requestContext, state.responseContext, detail, index, suggestionState, details.size) - 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 { - "" + decisions.add(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 = CodeWhispererService.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 = if (hasUserAccepted) recommendation.content() else "" + 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 + ) + + // 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) } - 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) + } } } @@ -446,74 +452,91 @@ class CodeWhispererTelemetryService { * - 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): CodewhispererPreviousSuggestionState { + fun aggregateUserDecision(decisions: List): CodewhispererSuggestionState { var isEmpty = true + var isUnseen = true + var isDiscard = true for (decision in decisions) { if (decision == CodewhispererSuggestionState.Accept) { - return CodewhispererPreviousSuggestionState.Accept + return CodewhispererSuggestionState.Accept } else if (decision == CodewhispererSuggestionState.Reject) { - return CodewhispererPreviousSuggestionState.Reject - } else if (decision != CodewhispererSuggestionState.Empty) { + 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) { - CodewhispererPreviousSuggestionState.Empty + CodewhispererSuggestionState.Empty + } else if (isDiscard) { + CodewhispererSuggestionState.Discard + } else if (isUnseen) { + CodewhispererSuggestionState.Unseen } else { - CodewhispererPreviousSuggestionState.Discard + CodewhispererSuggestionState.Ignore } } - 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, - ) - } + // TODO: Decide the scope of this telemetry +// 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, +// ) +// } + + // TODO: decide the scope of this telemetry +// 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. @@ -521,21 +544,17 @@ class CodeWhispererTelemetryService { } fun recordSuggestionState( - index: Int, - selectedIndex: Int, - hasSeen: Boolean, + detail: DetailContext, hasUserAccepted: Boolean, - isDiscarded: Boolean, - isEmpty: Boolean ): CodewhispererSuggestionState = - if (isEmpty) { + if (detail.recommendation.content().isEmpty()) { CodewhispererSuggestionState.Empty - } else if (isDiscarded) { + } else if (detail.isDiscarded) { CodewhispererSuggestionState.Discard - } else if (!hasSeen) { + } else if (!detail.hasSeen) { CodewhispererSuggestionState.Unseen } else if (hasUserAccepted) { - if (selectedIndex == index) { + if (detail.isAccepted) { CodewhispererSuggestionState.Accept } else { CodewhispererSuggestionState.Ignore diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt index 9576c002e05..1775631587b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt @@ -5,14 +5,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow import com.intellij.openapi.editor.RangeMarker import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +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.popup.CodeWhispererUserActionListener class CodeWhispererCodeReferenceActionListener : CodeWhispererUserActionListener { - override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) { - val (project, editor) = states.requestContext - val manager = CodeWhispererCodeReferenceManager.getInstance(project) - manager.insertCodeReference(states, sessionContext.selectedIndex) - manager.addListeners(editor) + override fun afterAccept(states: InvocationContext, previews: List, sessionContext: SessionContext, rangeMarker: RangeMarker) { + val manager = CodeWhispererCodeReferenceManager.getInstance(sessionContext.project) + manager.insertCodeReference(states, previews, sessionContext.selectedIndex) + manager.addListeners(sessionContext.editor) } } 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 f1ffacc8d2b..4d4df9b43bf 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 @@ -31,6 +31,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhisper import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints 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.PreviewContext import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.EDITOR_CODE_REFERENCE_HOVER import software.aws.toolkits.resources.message import javax.swing.JLabel @@ -109,11 +110,15 @@ 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) + fun insertCodeReference(states: InvocationContext, previews: List, selectedIndex: Int) { + val detail = previews[selectedIndex].detail + insertCodeReference( + detail.recommendation.content(), + detail.reformatted.references(), + states.requestContext.editor, + states.requestContext.caretPosition, + detail.recommendation + ) } fun getReferenceLineNums(editor: Editor, start: Int, end: Int): String { 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 4eb9980e216..519cd599bed 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 @@ -27,7 +27,7 @@ object CodeWhispererConstants { 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: Long = 50 const val POPUP_DELAY_CHECK_INTERVAL: Long = 25 const val IDLE_TIME_CHECK_INTERVAL: Long = 25 const val SUPPLEMENTAL_CONTEXT_TIMEOUT = 50L @@ -36,9 +36,9 @@ object CodeWhispererConstants { val AWSTemplateKeyWordsRegex = Regex("(AWSTemplateFormatVersion|Resources|AWS::|Description)") val AWSTemplateCaseInsensitiveKeyWordsRegex = Regex("(cloudformation|cfn|template|description)") - // 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 + // TODO: this is currently set to 0 to trigger with 0ms delay and rely on ML trigger to determine which characters to trigger at + // We will monitor service side resource utilization and throttling. + const val INVOCATION_DELAY: Long = 0 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" 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 7f48c50142a..ada04ea915d 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,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.util +import com.intellij.codeInsight.lookup.LookupManager import com.intellij.ide.BrowserUtil import com.intellij.notification.NotificationAction import com.intellij.openapi.application.ApplicationManager @@ -12,6 +13,8 @@ import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.ComponentUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -307,6 +310,12 @@ object CodeWhispererUtil { } } } + + fun setIntelliSensePopupAlpha(editor: Editor, alpha: Float) { + ComponentUtil.getWindow(LookupManager.getActiveLookup(editor)?.component)?.let { + WindowManager.getInstance().setAlphaModeRatio(it, alpha) + } + } } enum class CaretMovement { 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 4026c090a72..a785644614f 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 @@ -8,11 +8,6 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TAB -import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -22,6 +17,9 @@ 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.ACTION_KEY_ACCEPT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_NEXT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_PREV import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.generateMockCompletionDetail import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaFileName import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaResponse @@ -125,9 +123,9 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer keyboard shortcuts should be prioritized to be executed`() { - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_TAB) - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_RIGHT) - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_LEFT) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_ACCEPT) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_PREV) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_NEXT) } private fun testCodeWhispererKeyboardShortcutShouldBePrioritized(actionId: String) { @@ -160,8 +158,8 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { // move the cursor to the correct trigger point (...void main) projectRule.fixture.editor.caretModel.moveToOffset(47) } - withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + withCodeWhispererServiceInvokedAndWait { session -> + val recommendation = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].detail.reformatted.content() val editor = projectRule.fixture.editor val expectedContext = buildContextWithRecommendation(recommendation + remaining) val startOffset = editor.caretModel.offset @@ -176,8 +174,9 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { private fun acceptHelper(useKeyboard: Boolean) { if (useKeyboard) { - EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_TAB) - .execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + ActionManager.getInstance().getAction(ACTION_KEY_ACCEPT).actionPerformed( + AnActionEvent.createFromDataContext("test", null, DataContext.EMPTY_CONTEXT) + ) } else { popupManagerSpy.popupComponents.acceptButton.doClick() } 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 e76a34d2e45..ae72562c7e7 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 @@ -82,6 +82,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWh 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.SessionContext 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 @@ -238,10 +239,10 @@ class CodeWhispererClientAdaptorTest { @Test fun sendUserTriggerDecisionTelemetry() { - val mockModelConfiguraotr = mock { + val mockModelConfigurator = mock { on { activeCustomization(any()) } doReturn CodeWhispererCustomization("fake-arn", "fake-name") } - ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfiguraotr, disposableRule.disposable) + ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfigurator, disposableRule.disposable) val file = projectRule.fixture.addFileToProject("main.java", "public class Main {}") runInEdtAndWait { @@ -252,10 +253,14 @@ class CodeWhispererClientAdaptorTest { projectRule.fixture.editor, projectRule.project, file, - LatencyContext(codewhispererEndToEndStart = 0, codewhispererEndToEndEnd = 20000000) ) sut.sendUserTriggerDecisionTelemetry( + SessionContext( + projectRule.project, + projectRule.fixture.editor, + latencyContext = LatencyContext(codewhispererEndToEndStart = 0, codewhispererEndToEndEnd = 20000000) + ), requestContext, ResponseContext("fake-session-id"), CodewhispererCompletionType.Line, @@ -374,6 +379,7 @@ class CodeWhispererClientAdaptorTest { fun `sendTelemetryEvent for userTriggerDecision respects telemetry optin status, for SSO users`() { sendTelemetryEventOptOutCheckHelper { sut.sendUserTriggerDecisionTelemetry( + SessionContext(projectRule.project, mock(), latencyContext = LatencyContext()), aRequestContext(projectRule.project), aResponseContext(), aCompletionType(), diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt index 8979a11fb32..0ff94e68fc4 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt @@ -53,6 +53,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages 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 @@ -174,12 +175,11 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov } }, null, - mock(), aString() ) val responseContext = ResponseContext("sessionId") val recommendationContext = RecommendationContext( - listOf( + mutableListOf( DetailContext( "requestId", pythonResponse.completions()[0], @@ -192,10 +192,11 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov ), "x, y", "x, y", - mock() + mock(), + 0 ) - invocationContext = InvocationContext(requestContext, responseContext, recommendationContext, mock()) - sessionContext = SessionContext() + invocationContext = InvocationContext(requestContext, responseContext, recommendationContext) + sessionContext = SessionContext(project, fixture.editor, latencyContext = LatencyContext()) // it is needed because referenceManager is listening to CODEWHISPERER_USER_ACTION_PERFORMED topic project.replaceService(CodeWhispererCodeReferenceManager::class.java, mock(), disposableRule.disposable) @@ -338,6 +339,7 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_USER_ACTION_PERFORMED).afterAccept( invocationContext, mock(), + SessionContext(project, fixture.editor, latencyContext = LatencyContext()), rangeMarkerMock ) 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 1e3d42ffff0..8f2e184d057 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 @@ -3,11 +3,13 @@ package software.aws.toolkits.jetbrains.services.codewhisperer +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.editor.actionSystem.EditorActionManager import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_NEXT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_PREV import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse import javax.swing.JButton @@ -34,10 +36,10 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { } private fun testNavigation(isReverse: Boolean, useKeyboard: Boolean = false) { - withCodeWhispererServiceInvokedAndWait { + withCodeWhispererServiceInvokedAndWait { session -> val indexChange = if (isReverse) -1 else 1 - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(0) + assertThat(session.selectedIndex).isEqualTo(0) val expectedCount = pythonResponse.completions().size var expectedSelectedIndex: Int @@ -58,7 +60,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { } } - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex) + assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex) assertThat(oppositeButton.isEnabled).isEqualTo(false) repeat(expectedCount - 1) { @@ -66,7 +68,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { navigateHelper(isReverse, useKeyboard) assertThat(oppositeButton.isEnabled).isEqualTo(true) expectedSelectedIndex = (expectedSelectedIndex + indexChange) % expectedCount - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex) + assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex) checkRecommendationInfoLabelText(expectedSelectedIndex + 1, expectedCount) } assertThat(navigationButton.isEnabled).isEqualTo(false) @@ -75,13 +77,13 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { private fun navigateHelper(isReverse: Boolean, useKeyboard: Boolean) { if (useKeyboard) { - val actionHandler = EditorActionManager.getInstance() + val actionHandler = ActionManager.getInstance() if (isReverse) { - val leftArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT) - leftArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + val leftArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_PREV) + leftArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null, DataContext.EMPTY_CONTEXT)) } else { - val rightArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT) - rightArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + val rightArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_NEXT) + rightArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null, DataContext.EMPTY_CONTEXT)) } } else { if (isReverse) { 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 index f186740f1e8..0a050493a56 100644 --- 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 @@ -56,8 +56,8 @@ class CodeWhispererReferencesTest : CodeWhispererTestBase() { } } - withCodeWhispererServiceInvokedAndWait { states -> - states.recommendationContext.details.forEach { + withCodeWhispererServiceInvokedAndWait { session -> + codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail }.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 index e4202cba6b3..5d83170927f 100644 --- 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 @@ -21,8 +21,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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] + withCodeWhispererServiceInvokedAndWait { + val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail assertThat(firstRecommendation.isDiscarded).isEqualTo(true) } } @@ -31,12 +31,12 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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] + withCodeWhispererServiceInvokedAndWait { _ -> + val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail assertThat(firstRecommendation.isDiscarded).isEqualTo(true) - val details = states.recommendationContext.details - details.forEach { - assertThat(it.reformatted.references().isEmpty()) + val details = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail } + details.forEach { detail -> + assertThat(detail.reformatted.references().isEmpty()) } } } @@ -47,10 +47,9 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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] + withCodeWhispererServiceInvokedAndWait { + val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + assertThat(firstDetail.isDiscarded).isEqualTo(false) val span = firstDetail.reformatted.references()[0].recommendationContentSpan() assertThat(span.start()).isEqualTo(0) assertThat(span.end()).isEqualTo(lastNewLineIndex) @@ -84,9 +83,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { } withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(false) - val firstDetail = states.recommendationContext.details[0] + val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + assertThat(firstDetail.isDiscarded).isEqualTo(false) val span = firstDetail.reformatted.references()[0].recommendationContentSpan() assertThat(span.start()).isEqualTo(0) assertThat(span.end()).isEqualTo(lastNewLineIndex) @@ -101,10 +99,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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) + withCodeWhispererServiceInvokedAndWait { session -> + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining) popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) + assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) } } @@ -115,8 +113,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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) + withCodeWhispererServiceInvokedAndWait { + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.recommendation.content()).isEqualTo(firstRecommendation) } } @@ -128,10 +126,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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) + withCodeWhispererServiceInvokedAndWait { session -> + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining) popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) + assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) } } @@ -142,8 +140,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { 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) + withCodeWhispererServiceInvokedAndWait { + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.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 4bff4965c3d..e55cd93ebe7 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 @@ -12,6 +12,7 @@ import com.intellij.testFramework.runInEdtAndWait import kotlinx.coroutines.async import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Rule @@ -93,6 +94,11 @@ class CodeWhispererServiceTest { projectRule.project.replaceService(AwsConnectionManager::class.java, mock(), disposableRule.disposable) } + @After + fun tearDown() { + sut.disposeDisplaySession(false) + } + @Test fun `getRequestContext should have supplementalContext and customizatioArn if they're present`() { whenever(customizationConfig.activeCustomization(projectRule.project)).thenReturn( @@ -124,8 +130,7 @@ class CodeWhispererServiceTest { TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), projectRule.fixture.editor, projectRule.project, - file, - LatencyContext() + file ) runTest { @@ -149,8 +154,7 @@ class CodeWhispererServiceTest { TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), projectRule.fixture.editor, projectRule.project, - file, - LatencyContext() + file ) assertThat(actual.supplementalContext).isNotNull @@ -182,12 +186,11 @@ class CodeWhispererServiceTest { fileContextInfo = mockFileContext, supplementalContextDeferred = async { mockSupContext }, connection = ToolkitConnectionManager.getInstance(projectRule.project).activeConnection(), - latencyContext = LatencyContext(), customizationArn = "fake-arn" ) ) - sut.invokeCodeWhispererInBackground(mockRequestContext).join() + sut.invokeCodeWhispererInBackground(mockRequestContext, 0, LatencyContext())?.join() verify(mockRequestContext, times(1)).awaitSupplementalContext() verify(clientFacade).generateCompletionsPaginator(any()) 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 bde3d169f52..004854730ad 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 @@ -18,14 +17,12 @@ import org.junit.Ignore 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.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.settings.CodeWhispererConfiguration import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.services.codewhisperer.status.CodeWhispererStatusBarWidgetFactory @@ -34,14 +31,11 @@ import kotlin.test.fail class CodeWhispererSettingsTest : CodeWhispererTestBase() { - private lateinit var codewhispererServiceSpy: CodeWhispererService private lateinit var toolWindowHeadlessManager: ToolWindowHeadlessManagerImpl @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) { @@ -83,7 +77,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 @@ -92,7 +86,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()) } } 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..93c93654008 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 @@ -12,13 +12,16 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispe import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.telemetry.CodewhispererLanguage import software.aws.toolkits.telemetry.CodewhispererTriggerType +import kotlin.test.assertNotNull class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets request metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> - val actualRequestContext = states.requestContext + withCodeWhispererServiceInvokedAndWait { session -> + val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId + val actualRequestContext = codewhispererService.ongoingRequestsContext[selectedJobId] + assertNotNull(actualRequestContext) val editor = projectRule.fixture.editor val (actualProject, actualEditor, actualTriggerTypeInfo, actualCaretPosition, actualFileContextInfo) = actualRequestContext val (actualCaretContext, actualFilename, actualProgrammingLanguage) = actualFileContextInfo @@ -46,8 +49,10 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets response metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> - val actualResponseContext = states.responseContext + withCodeWhispererServiceInvokedAndWait { session -> + val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId + val actualResponseContext = codewhispererService.getAllPaginationSessions()[selectedJobId]?.responseContext + assertNotNull(actualResponseContext) assertThat(listOf(actualResponseContext.sessionId)).isEqualTo( pythonResponse.sdkHttpResponse().headers()[CodeWhispererService.KET_SESSION_ID] ) @@ -56,7 +61,9 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets recommendation metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { + val states = codewhispererService.getAllPaginationSessions()[0] + assertNotNull(states) val actualRecommendationContext = states.recommendationContext val (actualDetailContexts, actualUserInput) = actualRecommendationContext @@ -74,15 +81,13 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets initial typeahead and selected index correctly`() { - withCodeWhispererServiceInvokedAndWait { - val sessionContext = popupManagerSpy.sessionContext - val actualSelectedIndex = sessionContext.selectedIndex - val actualTypeahead = sessionContext.typeahead - val actualTypeaheadOriginal = sessionContext.typeaheadOriginal + withCodeWhispererServiceInvokedAndWait { session -> + val actualSelectedIndex = session.selectedIndex + val preview = codewhispererService.getAllSuggestionsPreviewInfo()[actualSelectedIndex] + val actualTypeahead = preview.typeahead assertThat(actualSelectedIndex).isEqualTo(0) assertThat(actualTypeahead).isEqualTo("") - assertThat(actualTypeaheadOriginal).isEqualTo("") } } } 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 index 263d1f81c72..c61515cabe4 100644 --- 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 @@ -37,8 +37,10 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererCon 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.model.InvocationContext 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.CodeWhispererService 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 @@ -86,7 +88,7 @@ class CodeWhispererTelemetryServiceTest { mockClient = spy(CodeWhispererClientAdaptor.getInstance(projectRule.project)) mockClient.stub { onGeneric { - sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any()) + sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any(), any()) }.doAnswer { mock() } @@ -97,25 +99,20 @@ class CodeWhispererTelemetryServiceTest { @After fun cleanup() { sut.previousDecisions().clear() + CodeWhispererService.getInstance().disposeDisplaySession(false) + CodeWhispererService.getInstance().getAllPaginationSessions().clear() } @Test fun `test recordSuggestionState`() { fun assertSuggestionStates(expectedStates: List) { - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(expectedStates) + val recommendationContext = aRecommendationContext(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() - ) + val suggestionState = sut.recordSuggestionState(detail, hasUserAccepted) actualStates.add(suggestionState) } @@ -156,7 +153,7 @@ class CodeWhispererTelemetryServiceTest { @Test fun `test aggregateUserDecision`() { - fun assertAggregateUserDecision(decisions: List, expected: CodewhispererPreviousSuggestionState) { + fun assertAggregateUserDecision(decisions: List, expected: CodewhispererSuggestionState) { val actual = sut.aggregateUserDecision(decisions) assertThat(actual).isEqualTo(expected) } @@ -169,7 +166,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Unseen, CodewhispererSuggestionState.Unseen ), - CodewhispererPreviousSuggestionState.Accept + CodewhispererSuggestionState.Accept ) assertAggregateUserDecision( @@ -180,7 +177,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Ignore ), - CodewhispererPreviousSuggestionState.Reject + CodewhispererSuggestionState.Reject ) assertAggregateUserDecision( @@ -191,7 +188,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Discard, CodewhispererSuggestionState.Empty ), - CodewhispererPreviousSuggestionState.Discard + CodewhispererSuggestionState.Discard ) assertAggregateUserDecision( @@ -201,7 +198,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Empty ), - CodewhispererPreviousSuggestionState.Empty + CodewhispererSuggestionState.Empty ) } @@ -219,6 +216,7 @@ class CodeWhispererTelemetryServiceTest { ) val supplementalContextInfo = aSupplementalContextInfo() + val sessionContext = aSessionContext(projectRule.project) val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also { runTest { it.awaitSupplementalContext() } } @@ -235,6 +233,7 @@ class CodeWhispererTelemetryServiceTest { } sut.sendUserTriggerDecisionEvent( + sessionContext, requestContext, responseContext, recommendationContext, @@ -252,7 +251,7 @@ class CodeWhispererTelemetryServiceTest { "codewhisperer_userTriggerDecision", 1, "codewhispererSessionId" to responseContext.sessionId, - "codewhispererFirstRequestId" to requestContext.latencyContext.firstRequestId, + "codewhispererFirstRequestId" to sessionContext.latencyContext.firstRequestId, "codewhispererCompletionType" to recommendationContext.details[0].completionType, "codewhispererLanguage" to requestContext.fileContextInfo.programmingLanguage.toTelemetryType(), "codewhispererTriggerType" to requestContext.triggerTypeInfo.triggerType, @@ -281,21 +280,29 @@ class CodeWhispererTelemetryServiceTest { @Test fun `sendUserDecisionEventForAll will send userDecision event for all suggestions`() { - doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any()) + doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), 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) + CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext( + aRequestContext(projectRule.project), + aResponseContext(), + aRecommendationContext(decisions) + ) val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept } val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration) + val sessionContext = aSessionContext(projectRule.project) + sessionContext.selectedIndex = 0 + sut.sendUserDecisionEventForAll( + sessionContext, + hasUserAccept, + popupShownDuration + ) argumentCaptor().apply { verify(batcher, atLeastOnce()).enqueue(capture()) @@ -348,12 +355,21 @@ class CodeWhispererTelemetryServiceTest { 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) + CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext( + requestContext, + aResponseContext(), + aRecommendationContext(decisions) + ) + val sessionContext = aSessionContext(projectRule.project) + sessionContext.selectedIndex = 0 + sut.sendUserDecisionEventForAll( + sessionContext, + hasUserAccept, + popupShownDuration + ) argumentCaptor().apply { verify(batcher, atLeastOnce()).enqueue(capture()) @@ -439,6 +455,7 @@ class CodeWhispererTelemetryServiceTest { ) AwsSettings.getInstance().isTelemetryEnabled = isTelemetryEnabled + val expectedSessionContext = aSessionContext(projectRule.project) val expectedRequestContext = aRequestContext(projectRule.project) val expectedResponseContext = aResponseContext() val expectedRecommendationContext = aRecommendationContext() @@ -449,6 +466,7 @@ class CodeWhispererTelemetryServiceTest { val expectedCharCount = 100 val expectedCompletionType = expectedRecommendationContext.details[0].completionType sut.sendUserTriggerDecisionEvent( + expectedSessionContext, expectedRequestContext, expectedResponseContext, expectedRecommendationContext, @@ -461,6 +479,7 @@ class CodeWhispererTelemetryServiceTest { if (isProTier || isTelemetryEnabled) { verify(mockClient).sendUserTriggerDecisionTelemetry( + eq(expectedSessionContext), eq(expectedRequestContext), eq(expectedResponseContext), eq(expectedCompletionType), 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 028008ce55f..ddd23bf52dc 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 @@ -7,7 +7,6 @@ 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 @@ -58,6 +57,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.R 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.model.PreviewContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext 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 @@ -73,6 +74,7 @@ import software.aws.toolkits.telemetry.CodewhispererSuggestionState import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Result import java.time.Instant +import kotlin.test.assertNotNull class CodeWhispererTelemetryTest : CodeWhispererTestBase() { private val userDecision = "codewhisperer_userDecision" @@ -105,12 +107,10 @@ 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()) } + codewhispererService.stub { + onGeneric { getRequestContext(any(), any(), any(), any()) } .doAnswer { throw Exception() } } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - invokeCodeWhispererService() argumentCaptor().apply { @@ -189,7 +189,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { @Test fun `test cancelling popup will send user decision event for all unseen but one rejected`() { withCodeWhispererServiceInvokedAndWait { states -> - popupManagerSpy.cancelPopup(states.popup) + codewhispererService.disposeDisplaySession(false) val count = pythonResponse.completions().size argumentCaptor().apply { @@ -373,68 +373,45 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { @Test fun `test invoking CodeWhisperer will send service invocation event with sessionId and requestId from response`() { - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { session -> val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) + val detail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + val states = codewhispererService.getAllPaginationSessions()[0] + assertNotNull(states) assertEventsContainsFieldsAndCount( metricCaptor.allValues, serviceInvocation, 1, "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, + "codewhispererRequestId" to detail.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(), any()) - val states = statesCaptor.lastValue + val sessionCaptor = argumentCaptor() + var states: InvocationContext? = null + var previews: List? = null + withCodeWhispererServiceInvokedAndWait { + states = codewhispererService.getAllPaginationSessions()[0] + previews = codewhispererService.getAllSuggestionsPreviewInfo() + } + verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(sessionCaptor.capture(), any(), any()) val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) + assertNotNull(states) + assertNotNull(previews) assertEventsContainsFieldsAndCount( metricCaptor.allValues, userDecision, - states.recommendationContext.details.size, - "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, + previews?.size ?: 0, + "codewhispererSessionId" to states?.responseContext?.sessionId, + "codewhispererRequestId" to previews?.get(0)?.detail?.requestId, ) } - @Test - fun `test showing IntelliSense after triggering CodeWhisperer will send userDecision events of state Discard`() { - val codewhispererServiceSpy = spy(codewhispererService) - codewhispererServiceSpy.stub { - onGeneric { - canDoInvocation(any(), any()) - } doAnswer { - true - } - } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - popupManagerSpy.stub { - onGeneric { - hasConflictingPopups(any()) - } doAnswer { - true - } - } - invokeCodeWhispererService() - - runInEdtAndWait { - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - pythonResponse.completions().size, - codewhispererSuggestionState to CodewhispererSuggestionState.Discard.toString(), - ) - } - } - @Test fun `test codePercentage tracker will not be activated if CWSPR terms of service is not accepted`() { val exploreManagerMock = mock { @@ -672,7 +649,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { } invokeCodeWhispererService() - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, never()).showPopup(any(), any()) runInEdtAndWait { val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) @@ -693,7 +670,9 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { } doReturnConsecutively(listOf(pythonResponseWithNonEmptyToken, emptyListResponse)) } - withCodeWhispererServiceInvokedAndWait { } + withCodeWhispererServiceInvokedAndWait { + popupManagerSpy.popupComponents.acceptButton.doClick() + } runInEdtAndWait { val metricCaptor = argumentCaptor() @@ -772,13 +751,12 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { val numOfEmptyRecommendations = response.completions().filter { it.content().isEmpty() }.size if (numOfEmptyRecommendations == response.completions().size) { - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, never()).showPopup(any(), any()) } else { - val popupCaptor = argumentCaptor() verify(popupManagerSpy, timeout(5000)) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + .showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } runInEdtAndWait { 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 6b76b28c939..02047fb6605 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 @@ -19,7 +19,6 @@ import org.junit.Rule import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doNothing import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.timeout @@ -41,7 +40,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhisper import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreStateType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager -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.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager @@ -97,9 +96,11 @@ open class CodeWhispererTestBase { } } + codewhispererService = spy(CodeWhispererService.getInstance()) + ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererService, disposableRule.disposable) + popupManagerSpy = spy(CodeWhispererPopupManager.getInstance()) - popupManagerSpy.reset() - doNothing().`when`(popupManagerSpy).showPopup(any(), any(), any(), any(), any()) + codewhispererService.disposeDisplaySession(false) ApplicationManager.getApplication().replaceService(CodeWhispererPopupManager::class.java, popupManagerSpy, disposableRule.disposable) invocationStatusSpy = spy(CodeWhispererInvocationStatus.getInstance()) @@ -114,7 +115,6 @@ open class CodeWhispererTestBase { stateManager = spy(CodeWhispererExplorerActionManager.getInstance()) recommendationManager = CodeWhispererRecommendationManager.getInstance() - codewhispererService = CodeWhispererService.getInstance() editorManager = CodeWhispererEditorManager.getInstance() settingsManager = CodeWhispererSettings.getInstance() @@ -153,24 +153,22 @@ open class CodeWhispererTestBase { open fun tearDown() { stateManager.loadState(originalExplorerActionState) settingsManager.loadState(originalSettings) - popupManagerSpy.reset() - runInEdtAndWait { - popupManagerSpy.closePopup() - } + codewhispererService.disposeDisplaySession(true) } - fun withCodeWhispererServiceInvokedAndWait(runnable: (InvocationContext) -> Unit) { - val statesCaptor = argumentCaptor() + fun withCodeWhispererServiceInvokedAndWait(runnable: (SessionContext) -> Unit) { + val sessionCaptor = argumentCaptor() invokeCodeWhispererService() verify(popupManagerSpy, timeout(5000).atLeastOnce()) - .showPopup(statesCaptor.capture(), any(), any(), any(), any()) - val states = statesCaptor.lastValue + .showPopup(sessionCaptor.capture(), any()) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) + val session = sessionCaptor.lastValue runInEdtAndWait { try { - runnable(states) + runnable(session) } finally { - CodeWhispererPopupManager.getInstance().closePopup(states.popup) + codewhispererService.disposeDisplaySession(true) } } @@ -194,7 +192,7 @@ open class CodeWhispererTestBase { jobRef.get()?.join() // wait for subsequent background operations to be complete - while (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) { + while (CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation()) { yield() delay(10) } 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 06db93b345c..50cc14009fe 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 @@ -34,58 +34,14 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() { testTypingTypeaheadMatchingRecommendationShouldMatchRightContext(testRightContext) } - @Test - fun `test typing blank typeahead should correctly update typeahead state`() { - val testTypeaheadOriginal = " " - testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly(testTypeaheadOriginal, 5, 1) - } - - @Test - fun `test typing typeahead with leading spaces and matching suffix should correctly update typeahead state`() { - val testTypeaheadOriginal = " test" - testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly(testTypeaheadOriginal, 3, 3) - } - - private fun testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly( - expectedTypeaheadOriginal: String, - expectedNumOfValidRecommendation: Int, - expectedSelectedAfterBackspace: Int - ) { - withCodeWhispererServiceInvokedAndWait { states -> - val editor = projectRule.fixture.editor - val startOffset = editor.caretModel.offset - expectedTypeaheadOriginal.forEach { char -> - projectRule.fixture.type(char) - val caretOffset = editor.caretModel.offset - val actualTypeaheadOriginal = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - val actualTypeahead = actualTypeaheadOriginal.trimStart() - assertThat(popupManagerSpy.sessionContext.typeaheadOriginal).isEqualTo(actualTypeaheadOriginal) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(actualTypeahead) - assertThat(states.popup.isDisposed).isFalse - } - checkRecommendationInfoLabelText(1, expectedNumOfValidRecommendation) - - // Backspacing for the same amount of times - expectedTypeaheadOriginal.forEach { _ -> - projectRule.fixture.type('\b') - val caretOffset = editor.caretModel.offset - val actualTypeaheadOriginal = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - val actualTypeahead = actualTypeaheadOriginal.trimStart() - assertThat(popupManagerSpy.sessionContext.typeaheadOriginal).isEqualTo(actualTypeaheadOriginal) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(actualTypeahead) - assertThat(states.popup.isDisposed).isFalse - } - checkRecommendationInfoLabelText(expectedSelectedAfterBackspace, 5) - } - } - private fun testTypingTypeaheadMatchingRecommendationShouldMatchRightContext(rightContext: String) { projectRule.fixture.configureByText(pythonFileName, pythonTestLeftContext + rightContext) runInEdtAndWait { projectRule.fixture.editor.caretModel.moveToOffset(pythonTestLeftContext.length) } - withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + withCodeWhispererServiceInvokedAndWait { session -> + var preview = codewhispererService.getAllSuggestionsPreviewInfo()[0] + val recommendation = preview.detail.reformatted.content() val editor = projectRule.fixture.editor val startOffset = editor.caretModel.offset recommendation.forEachIndexed { index, char -> @@ -93,7 +49,8 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() { projectRule.fixture.type(char) val caretOffset = editor.caretModel.offset val typeahead = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead) + preview = codewhispererService.getAllSuggestionsPreviewInfo()[0] + assertThat(preview.typeahead).isEqualTo(typeahead) } } } 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 d46a603e61b..c08238b7a17 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 @@ -14,14 +14,12 @@ import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TEXT_END_WITH_ import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TEXT_START_WITH_SELECTION import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.event.VisibleAreaEvent -import com.intellij.openapi.ui.popup.JBPopup import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mockito.times import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.timeout @@ -95,7 +93,7 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { } withCodeWhispererServiceInvokedAndWait { projectRule.fixture.performEditorAction(actionId) - verify(popupManagerSpy, timeout(5000)).cancelPopup(any()) + verify(codewhispererService, timeout(5000).atLeastOnce()).disposeDisplaySession(false) } } @@ -120,11 +118,9 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { projectRule.fixture.type('\n') val expectedFileContext = "$testLeftContext\n \n $testRightContext" assertThat(projectRule.fixture.editor.document.text).isEqualTo(expectedFileContext) - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000)) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + verify(popupManagerSpy, timeout(5000)).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } @@ -138,10 +134,10 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { on { this.newRectangle } doReturn newRect } withCodeWhispererServiceInvokedAndWait { states -> - CodeWhispererInvocationStatus.getInstance().setPopupActive(true) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) val listener = CodeWhispererScrollListener(states) listener.visibleAreaChanged(event) - verify(popupManagerSpy, times(2)).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, times(2)).showPopup(any(), any()) } } @@ -163,15 +159,12 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { setFileContext(pythonFileName, "def", rightContext) projectRule.fixture.type('{') if (shouldtrigger) { - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000).atLeastOnce()) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + verify(popupManagerSpy, timeout(5000).atLeastOnce()).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } else { - verify(popupManagerSpy, times(0)) - .showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, times(0)).showPopup(any(), any()) } } @@ -179,11 +172,9 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { 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(), any()) + verify(popupManagerSpy, timeout(5000).atLeast(times)).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } } 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 8c4c562481e..f534eeb37b6 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 @@ -3,21 +3,16 @@ package software.aws.toolkits.jetbrains.services.codewhisperer -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile -import com.intellij.testFramework.replaceService import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.spy import org.mockito.kotlin.stub import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse -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.CodeWhispererService class CodeWhispererUserInputTest : CodeWhispererTestBase() { @@ -25,10 +20,8 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { fun `test no user input should show all recommendations`() { addUserInputAfterInvocation("") - withCodeWhispererServiceInvokedAndWait { states -> - val actualRecommendations = states.recommendationContext.details.map { - it.recommendation.content() - } + withCodeWhispererServiceInvokedAndWait { session -> + val actualRecommendations = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail.recommendation.content() } assertThat(actualRecommendations).isEqualTo(pythonResponse.completions().map { it.content() }) } } @@ -40,10 +33,11 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { val expectedRecommendations = pythonResponse.completions().map { it.content() } - withCodeWhispererServiceInvokedAndWait { states -> - val actualRecommendations = states.recommendationContext.details.map { it.recommendation.content() } + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + val actualRecommendations = previews.map { it.detail.recommendation.content() } assertThat(actualRecommendations).isEqualTo(expectedRecommendations) - states.recommendationContext.details.forEachIndexed { index, context -> + previews.map { it.detail }.forEachIndexed { index, context -> val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) val actualDiscarded = context.isDiscarded assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -58,10 +52,11 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { val typeahead = " recommendation" - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { session -> projectRule.fixture.type(typeahead) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead) - states.recommendationContext.details.forEachIndexed { index, actualContext -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].typeahead).isEqualTo(typeahead) + previews.map { it.detail }.forEachIndexed { index, actualContext -> val actualDiscarded = actualContext.isDiscarded val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput + typeahead) assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -75,9 +70,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { addUserInputAfterInvocation(blankUserInput) val userInput = blankUserInput.trimStart() - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { _, actualContext -> + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput) + previews.map { it.detail }.forEachIndexed { _, actualContext -> assertThat(actualContext.isDiscarded).isEqualTo(false) } } @@ -89,9 +85,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { addUserInputAfterInvocation(userInputWithLeadingSpaces) val userInput = userInputWithLeadingSpaces.trimStart() - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { index, actualContext -> + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput) + previews.map { it.detail }.forEachIndexed { index, actualContext -> val actualDiscarded = actualContext.isDiscarded val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -100,33 +97,28 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { } private 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(), editorCaptor.capture(), projectCaptor.capture(), - psiFileCaptor.capture(), - latencyContextCaptor.capture() + psiFileCaptor.capture() ) }.doAnswer { - val requestContext = codewhispererServiceSpy.getRequestContext( + val requestContext = codewhispererService.getRequestContext( triggerTypeCaptor.firstValue, editorCaptor.firstValue, projectCaptor.firstValue, psiFileCaptor.firstValue, - latencyContextCaptor.firstValue ) projectRule.fixture.type(userInput) requestContext }.thenCallRealMethod() } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt index 5dcbaaacc6f..85ff06206dd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.project.Project import kotlinx.coroutines.async @@ -69,6 +70,9 @@ object CodeWhispererTestUtil { const val codeWhispererCodeScanActionId = "codewhisperer.toolbar.security.scan" const val testValidAccessToken = "test_valid_access_token" const val testNextToken = "test_next_token" + const val ACTION_KEY_ACCEPT = "codewhisperer.inline.accept" + const val ACTION_KEY_NAV_PREV = "codewhisperer.inline.navigate.previous" + const val ACTION_KEY_NAV_NEXT = "codewhisperer.inline.navigate.next" private val testReferenceInfoPair = listOf( Pair("MIT", "testRepo1"), Pair("Apache-2.0", "testRepo2"), @@ -211,6 +215,12 @@ object CodeWhispererTestUtil { .build() } +fun aSessionContext( + project: Project = mock(), + editor: Editor = mock(), + latencyContext: LatencyContext = LatencyContext() +) = SessionContext(project, editor, latencyContext = latencyContext) + fun aRequestContext( project: Project, myFileContextInfo: FileContextInfo? = null, @@ -242,20 +252,6 @@ fun aRequestContext( fileContextInfo = myFileContextInfo ?: aFileContextInfo(), supplementalContextDeferred = supplementalContextDeferred, null, - LatencyContext( - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextDouble(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - aString() - ), customizationArn = null ) } @@ -306,14 +302,15 @@ fun aRecommendationContext(): RecommendationContext { details, aString(), aString(), - VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)) + VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)), + 0 ) } /** * util to generate a RecommendationContext and a SessionContext given expected decisions */ -fun aRecommendationContextAndSessionContext(decisions: List): Pair { +fun aRecommendationContext(decisions: List): RecommendationContext { val table = CodewhispererSuggestionState.values().associateWith { 0 }.toMutableMap() decisions.forEach { table[it]?.let { curCount -> table[it] = 1 + curCount } @@ -331,6 +328,8 @@ fun aRecommendationContextAndSessionContext(decisions: List() - decisions.forEachIndexed { index, decision -> - if (decision != CodewhispererSuggestionState.Unseen) { - seen.add(index) - } - } - - val sessionContext = SessionContext( - selectedIndex = selectedIndex, - seen = seen - ) - return recommendationContext to sessionContext + return recommendationContext } fun aCompletion(content: String? = null, isEmpty: Boolean = false, referenceCount: Int? = null, importCount: Int? = null): Completion { diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt index 80dea49e17c..9e25cfdf542 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt @@ -38,14 +38,16 @@ import software.aws.toolkits.telemetry.ToolkitTelemetry class PluginUpdateManager : Disposable { private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this) + fun isBeta() = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.descriptor?.version?.contains("beta") ?: false + fun scheduleAutoUpdate() { if (alarm.isDisposed) return scheduleUpdateTask() val enabled = AwsSettings.getInstance().isAutoUpdateEnabled - LOG.debug { "AWS plugins checking for new updates. Auto update enabled: $enabled" } + LOG.debug { "AWS plugins checking for new updates. Auto update enabled: $enabled, isBeta: ${isBeta()}" } - if (!enabled) return + if (!enabled && !isBeta()) return runInEdt { ProgressManager.getInstance().run(object : Task.Backgroundable( diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 867cf7178b0..34462ecefbd 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -767,6 +767,8 @@ codemodernizer.toolwindow.transformation.progress.running_time=Running time: {0} codewhisperer.actions.connect_github.title=Connect with Us on GitHub codewhisperer.actions.open_settings.title=Open Settings codewhisperer.actions.send_feedback.title=Send Feedback +codewhisperer.actions.switch_to_marketplace.progress.title=Switching to marketplace version +codewhisperer.actions.switch_to_marketplace.title=Switch Back to Marketplace codewhisperer.actions.view_documentation.title=View Documentation codewhisperer.codescan.apply_fix_button_label=Apply fix codewhisperer.codescan.apply_fix_button_tooltip=Apply suggested fix @@ -853,6 +855,10 @@ codewhisperer.gettingstarted.panel.learn_more=Learn more codewhisperer.gettingstarted.panel.learn_more.with.q=Learn more about Amazon Q and Codewhisperer codewhisperer.gettingstarted.panel.licence_comment=Already have a license? codewhisperer.gettingstarted.panel.login_button=Use for free, no AWS account required +codewhisperer.inline.accept=Accept Suggestion +codewhisperer.inline.force.accept=Force Accept Suggestion +codewhisperer.inline.navigate.next=Navigate to Next Suggestion +codewhisperer.inline.navigate.previous=Navigate to Previous Suggestion codewhisperer.language.error={0} is currently not supported by Amazon Q codewhisperer.learn_page.banner.dismiss=Dismiss codewhisperer.learn_page.banner.message.new_user=You can always return to this page by clicking "Learn" in the Amazon Q status bar menu. @@ -875,13 +881,16 @@ codewhisperer.notification.custom.not_available=Selected Amazon Q customization codewhisperer.notification.custom.simple.button.got_it=Got it codewhisperer.notification.custom.simple.button.select_another_customization=Select another customization codewhisperer.notification.custom.simple.button.select_customization=Select customization +codewhisperer.notification.inline.shortcut_config.content=Check out the default and customize in the keymap settings. +codewhisperer.notification.inline.shortcut_config.open_setting=Open keymap settings +codewhisperer.notification.inline.shortcut_config.title=Configurable Amazon Q suggestion shortcuts codewhisperer.notification.remote.ide_unsupported.message=Please update your IDE backend to a 2023.3 or later version to continue using Amazon Q inline suggestions. codewhisperer.notification.remote.ide_unsupported.title=Amazon Q inline suggestion not supported in this IDE version codewhisperer.notification.usage_limit.codescan.warn.content=Amazon Q: You have reached the monthly limit for project scans. codewhisperer.notification.usage_limit.codesuggestion.warn.content=You have reached the monthly fair use limit of code recommendations. codewhisperer.popup.button.accept=
 Insert Code 
\u21E5
-codewhisperer.popup.button.next=
Next
-codewhisperer.popup.button.prev=
Previous
+codewhisperer.popup.button.next=
Next
{1}
+codewhisperer.popup.button.prev=
Previous
{1}
codewhisperer.popup.import_info=

If you insert code, "{0}"{2, choice, 0#|1# and {1} other import|2# and {1} other imports} will also be added.

codewhisperer.popup.no_recommendations=No Amazon Q recommendations available at this point codewhisperer.popup.pagination_info=Loading additional Amazon Q suggestions... @@ -1502,6 +1511,10 @@ loading_resource.still_loading=Resources are still loading plugin.incompatible.fix=Disable incompatible plugins and restart IDE plugin.incompatible.message=The plugin versions for Amazon Q, AWS Toolkit, and AWS Toolkit Core must match or conflicts may occur. plugin.incompatible.title=AWS Plugin Incompatibility +q.beta.notification.end.message="The current beta period has ended on {0}, please switch to the marketplace version to continue using Amazon Q." +q.beta.notification.end.title="Amazon Q current beta period ended" +q.beta.notification.welcome.message="Thank you for participating in Amazon Q beta plugin testing. Plugin auto-update is always turned on to ensure the best beta experience." +q.beta.notification.welcome.title="Welcome to Amazon Q Plugin Beta" q.connection.disconnected=You don't have access to Amazon Q. Please authenticate to get started. q.connection.expired=Your Amazon Q session has timed out. Re-authenticate to continue. q.connection.invalid=You don't have access to Amazon Q. Please authenticate to get started.