From 824566e6106b1da0510a85ea709745af322ea4b5 Mon Sep 17 00:00:00 2001
From: andrewyuq <89420755+andrewyuq@users.noreply.github.com>
Date: Tue, 26 Nov 2024 11:27:24 -0800
Subject: [PATCH] Revert new auto-trigger UX (#5127)
* Revert "Fix perceivedLatency to set it for only 1 trigger per display session (#5118)"
This reverts commit 364305eb786c8c5a857045caa4e185c0e9a4026b.
* Revert "feat(amazonq): Introduce auto trigger changes officially (#5080)"
This reverts commit b01283e81102b32b6b4a93172329c935f53a95bd.
---------
Co-authored-by: Lei Gao <97199248+leigaol@users.noreply.github.com>
---
.../META-INF/plugin-codewhisperer.xml | 4 +
.../actions/CodeWhispererAcceptAction.kt | 10 +-
.../actions/CodeWhispererActionPromoter.kt | 72 +-
.../CodeWhispererNavigateNextAction.kt | 10 +-
.../CodeWhispererNavigatePrevAction.kt | 10 +-
.../CodeWhispererRecommendationAction.kt | 17 +-
.../credentials/CodeWhispererClientAdaptor.kt | 82 +-
.../editor/CodeWhispererEditorListener.kt | 8 +-
.../editor/CodeWhispererEditorManager.kt | 49 +-
.../editor/CodeWhispererEditorManagerNew.kt | 282 ++++++
.../importadder/CodeWhispererImportAdder.kt | 45 +-
.../CodeWhispererImportAdderListener.kt | 22 +-
.../inlay/CodeWhispererInlayManager.kt | 92 ++
.../inlay/CodeWhispererInlayManagerNew.kt | 6 +-
.../codewhisperer/model/CodeWhispererModel.kt | 106 ++-
.../popup/CodeWhispererPopupListener.kt | 33 +-
.../popup/CodeWhispererPopupManager.kt | 576 ++++++-----
.../popup/CodeWhispererPopupManagerNew.kt | 630 ++++++++++++
.../popup/CodeWhispererUIChangeListener.kt | 67 +-
.../popup/CodeWhispererUIChangeListenerNew.kt | 108 +++
.../CodeWhispererEditorActionHandler.kt | 6 +-
.../CodeWhispererPopupBackspaceHandler.kt | 26 +-
.../CodeWhispererPopupEnterHandler.kt | 26 +-
.../handlers/CodeWhispererPopupEscHandler.kt | 8 +-
.../CodeWhispererPopupLeftArrowHandler.kt | 19 +
.../CodeWhispererPopupRightArrowHandler.kt | 19 +
.../handlers/CodeWhispererPopupTabHandler.kt | 19 +
.../CodeWhispererPopupTypedHandler.kt | 20 +-
...CodeWhispererAcceptButtonActionListener.kt | 13 +-
.../listeners/CodeWhispererActionListener.kt | 6 +-
.../CodeWhispererNextButtonActionListener.kt | 13 +-
.../CodeWhispererPrevButtonActionListener.kt | 13 +-
.../listeners/CodeWhispererScrollListener.kt | 22 +-
.../CodeWhispererAutoTriggerHandler.kt | 7 +-
.../CodeWhispererAutoTriggerService.kt | 18 +-
.../service/CodeWhispererInvocationStatus.kt | 37 +-
.../CodeWhispererInvocationStatusNew.kt | 81 ++
.../CodeWhispererRecommendationManager.kt | 118 ++-
.../service/CodeWhispererService.kt | 681 ++++++-------
.../service/CodeWhispererServiceNew.kt | 898 ++++++++++++++++++
.../settings/CodeWhispererConfigurable.kt | 23 +-
...hispererIntelliSenseAutoTriggerListener.kt | 31 +-
.../status/CodeWhispererStatusBarWidget.kt | 10 +-
.../CodeWhispererCodeCoverageTracker.kt | 67 +-
.../CodeWhispererTelemetryService.kt | 214 +++--
.../CodeWhispererTelemetryServiceNew.kt | 520 ++++++++++
...odeWhispererCodeReferenceActionListener.kt | 13 +-
.../CodeWhispererCodeReferenceManager.kt | 3 +-
.../codewhisperer/CodeWhispererAcceptTest.kt | 24 +-
.../CodeWhispererClientAdaptorTest.kt | 18 +-
.../CodeWhispererCodeCoverageTrackerTest.kt | 17 +-
.../CodeWhispererNavigationTest.kt | 25 +-
.../CodeWhispererRecommendationManagerTest.kt | 2 +-
.../CodeWhispererReferencesTest.kt | 4 +-
.../CodeWhispererRightContextTest.kt | 46 +-
.../codewhisperer/CodeWhispererServiceTest.kt | 45 +-
.../CodeWhispererSettingsTest.kt | 10 +-
.../codewhisperer/CodeWhispererStateTest.kt | 27 +-
.../CodeWhispererTelemetryServiceTest.kt | 90 +-
.../CodeWhispererTelemetryTest.kt | 90 +-
.../codewhisperer/CodeWhispererTestBase.kt | 28 +-
.../CodeWhispererTypeaheadTest.kt | 53 +-
.../CodeWhispererUserActionsTest.kt | 29 +-
.../CodeWhispererUserInputTest.kt | 50 +-
.../codewhisperer/CodeWhispererTestUtil.kt | 68 +-
.../CodeWhispererFeatureConfigService.kt | 16 +
66 files changed, 4594 insertions(+), 1208 deletions(-)
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupLeftArrowHandler.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupRightArrowHandler.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTabHandler.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatusNew.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt
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 00bee1e0ed..0c3e05ea1f 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
@@ -5,8 +5,12 @@
+
+
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererAcceptAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererAcceptAction.kt
index cce45029ba..d1c96ed0d6 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererAcceptAction.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererAcceptAction.kt
@@ -10,8 +10,8 @@ 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.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
import software.aws.toolkits.resources.message
open class CodeWhispererAcceptAction(title: String = message("codewhisperer.inline.accept")) : AnAction(title), DumbAware {
@@ -19,12 +19,12 @@ open class CodeWhispererAcceptAction(title: String = message("codewhisperer.inli
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.EDITOR) != null &&
- CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()
+ CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()
}
override fun actionPerformed(e: AnActionEvent) {
- val sessionContext = e.project?.getUserData(CodeWhispererService.KEY_SESSION_CONTEXT) ?: return
- if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return
+ val sessionContext = e.project?.getUserData(CodeWhispererServiceNew.KEY_SESSION_CONTEXT) ?: return
+ if (!CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) return
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
).beforeAccept(sessionContext)
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererActionPromoter.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererActionPromoter.kt
index 59ee7dcd7c..8c81f986f5 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererActionPromoter.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererActionPromoter.kt
@@ -7,45 +7,77 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.openapi.actionSystem.ActionPromoter
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DataContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
+import com.intellij.openapi.editor.actionSystem.EditorAction
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
+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.service.CodeWhispererInvocationStatusNew
class CodeWhispererActionPromoter : ActionPromoter {
override fun promote(actions: MutableList, context: DataContext): MutableList {
- val results = actions.toMutableList()
- if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return results
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ val results = actions.toMutableList()
+ if (!CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) return results
- results.sortWith { a, b ->
- if (isCodeWhispererForceAction(a)) {
- return@sortWith -1
- } else if (isCodeWhispererForceAction(b)) {
- return@sortWith 1
- }
+ results.sortWith { a, b ->
+ if (isCodeWhispererForceAction(a)) {
+ return@sortWith -1
+ } else if (isCodeWhispererForceAction(b)) {
+ return@sortWith 1
+ }
- if (a is ChooseItemAction) {
- return@sortWith -1
- } else if (b is ChooseItemAction) {
- return@sortWith 1
- }
+ if (a is ChooseItemAction) {
+ return@sortWith -1
+ } else if (b is ChooseItemAction) {
+ return@sortWith 1
+ }
+
+ if (isCodeWhispererAcceptAction(a)) {
+ return@sortWith -1
+ } else if (isCodeWhispererAcceptAction(b)) {
+ return@sortWith 1
+ }
- if (isCodeWhispererAcceptAction(a)) {
+ 0
+ }
+ return results
+ }
+ val results = actions.toMutableList()
+ results.sortWith { a, b ->
+ if (isCodeWhispererPopupAction(a)) {
return@sortWith -1
- } else if (isCodeWhispererAcceptAction(b)) {
+ } else if (isCodeWhispererPopupAction(b)) {
return@sortWith 1
+ } else {
+ 0
}
-
- 0
}
return results
}
private fun isCodeWhispererAcceptAction(action: AnAction): Boolean =
- action is CodeWhispererAcceptAction
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ action is CodeWhispererAcceptAction
+ } else {
+ action is EditorAction && action.handler is CodeWhispererPopupTabHandler
+ }
private fun isCodeWhispererForceAcceptAction(action: AnAction): Boolean =
action is CodeWhispererForceAcceptAction
private fun isCodeWhispererNavigateAction(action: AnAction): Boolean =
- action is CodeWhispererNavigateNextAction || action is CodeWhispererNavigatePrevAction
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ action is CodeWhispererNavigateNextAction || action is CodeWhispererNavigatePrevAction
+ } else {
+ action is EditorAction && (
+ action.handler is CodeWhispererPopupRightArrowHandler ||
+ action.handler is CodeWhispererPopupLeftArrowHandler
+ )
+ }
+
+ 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
index ed02c8803a..9efb2d0f18 100644
--- 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
@@ -10,8 +10,8 @@ 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.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
import software.aws.toolkits.resources.message
class CodeWhispererNavigateNextAction : AnAction(message("codewhisperer.inline.navigate.next")), DumbAware {
@@ -20,12 +20,12 @@ class CodeWhispererNavigateNextAction : AnAction(message("codewhisperer.inline.n
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = e.project != null &&
e.getData(CommonDataKeys.EDITOR) != null &&
- CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()
+ CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()
}
override fun actionPerformed(e: AnActionEvent) {
- val sessionContext = e.project?.getUserData(CodeWhispererService.KEY_SESSION_CONTEXT) ?: return
- if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return
+ val sessionContext = e.project?.getUserData(CodeWhispererServiceNew.KEY_SESSION_CONTEXT) ?: return
+ if (!CodeWhispererInvocationStatusNew.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
index 118f773821..633dd52137 100644
--- 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
@@ -10,8 +10,8 @@ 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.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
import software.aws.toolkits.resources.message
class CodeWhispererNavigatePrevAction : AnAction(message("codewhisperer.inline.navigate.previous")), DumbAware {
@@ -20,12 +20,12 @@ class CodeWhispererNavigatePrevAction : AnAction(message("codewhisperer.inline.n
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = e.project != null &&
e.getData(CommonDataKeys.EDITOR) != null &&
- CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()
+ CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()
}
override fun actionPerformed(e: AnActionEvent) {
- val sessionContext = e.project?.getUserData(CodeWhispererService.KEY_SESSION_CONTEXT) ?: return
- if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) return
+ val sessionContext = e.project?.getUserData(CodeWhispererServiceNew.KEY_SESSION_CONTEXT) ?: return
+ if (!CodeWhispererInvocationStatusNew.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/CodeWhispererRecommendationAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt
index c5f4a54670..97fcc101b8 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt
@@ -10,10 +10,12 @@ import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.Key
import kotlinx.coroutines.Job
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererTriggerType
import java.util.concurrent.atomic.AtomicReference
@@ -30,12 +32,23 @@ class CodeWhispererRecommendationAction : AnAction(message("codewhisperer.trigge
latencyContext.codewhispererPreprocessingStart = System.nanoTime()
latencyContext.codewhispererEndToEndStart = System.nanoTime()
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
- if (!CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.OnDemand)) {
+ if (!(
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererServiceNew.getInstance().canDoInvocation(editor, CodewhispererTriggerType.OnDemand)
+ } else {
+ CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.OnDemand)
+ }
+ )
+ ) {
return
}
val triggerType = TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown())
- val job = CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerType, latencyContext)
+ val job = if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererServiceNew.getInstance().showRecommendationsInPopup(editor, triggerType, latencyContext)
+ } else {
+ CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerType, latencyContext)
+ }
e.getData(CommonDataKeys.EDITOR)?.getUserData(ACTION_JOB_KEY)?.set(job)
}
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 7f3ed287ca..27feadf917 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
@@ -44,9 +44,10 @@ import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext
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.RequestContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.ResponseContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference
import software.aws.toolkits.jetbrains.services.codewhisperer.util.transform
@@ -89,7 +90,6 @@ interface CodeWhispererClientAdaptor : Disposable {
fun listAvailableCustomizations(): List
fun sendUserTriggerDecisionTelemetry(
- sessionContext: SessionContext,
requestContext: RequestContext,
responseContext: ResponseContext,
completionType: CodewhispererCompletionType,
@@ -100,6 +100,18 @@ interface CodeWhispererClientAdaptor : Disposable {
acceptedCharCount: Int,
): SendTelemetryEventResponse
+ fun sendUserTriggerDecisionTelemetry(
+ sessionContext: SessionContextNew,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ completionType: CodewhispererCompletionType,
+ suggestionState: CodewhispererSuggestionState,
+ suggestionReferenceCount: Int,
+ lineCount: Int,
+ numberOfRecommendations: Int,
+ acceptedCharCount: Int,
+ ): SendTelemetryEventResponse
+
fun sendCodePercentageTelemetry(
language: CodeWhispererProgrammingLanguage,
customizationArn: String?,
@@ -290,7 +302,6 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
}
override fun sendUserTriggerDecisionTelemetry(
- sessionContext: SessionContext,
requestContext: RequestContext,
responseContext: ResponseContext,
completionType: CodewhispererCompletionType,
@@ -302,19 +313,54 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
): SendTelemetryEventResponse {
val fileContext = requestContext.fileContextInfo
val programmingLanguage = fileContext.programmingLanguage
- var e2eLatency = sessionContext.latencyContext.getCodeWhispererEndToEndLatency()
+ var e2eLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency()
+
+ // When we send a userTriggerDecision for neither Accept nor Reject, service side should not use this value
+ // and client side will set this value to 0.0.
+ if (suggestionState != CodewhispererSuggestionState.Accept &&
+ suggestionState != CodewhispererSuggestionState.Reject
+ ) {
+ e2eLatency = 0.0
+ }
- // service side will only aggregate perceivedLatency with non-zero value
- // For client-side, if the decision is not accept and reject, we will set the value to 0
- // If the decision is accept or reject, it's guaranteed that they will need a perceivedLatency
- // of non-zero because for accept case it's trivial, for reject case, this trigger must be the
- // first seen trigger and this is the only one reject in this display session.
- val emittedPerceivedLatency =
- if (suggestionState == CodewhispererSuggestionState.Accept || suggestionState == CodewhispererSuggestionState.Reject) {
- sessionContext.latencyContext.perceivedLatency
- } else {
- 0.0
+ return bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.userTriggerDecisionEvent {
+ it.requestId(requestContext.latencyContext.firstRequestId)
+ it.completionType(completionType.toCodeWhispererSdkType())
+ it.programmingLanguage { builder -> builder.languageName(programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) }
+ it.sessionId(responseContext.sessionId)
+ it.recommendationLatencyMilliseconds(e2eLatency)
+ it.triggerToResponseLatencyMilliseconds(requestContext.latencyContext.paginationFirstCompletionTime)
+ it.perceivedLatencyMilliseconds(requestContext.latencyContext.perceivedLatency)
+ it.suggestionState(suggestionState.toCodeWhispererSdkType())
+ it.timestamp(Instant.now())
+ it.suggestionReferenceCount(suggestionReferenceCount)
+ it.generatedLine(lineCount)
+ it.customizationArn(requestContext.customizationArn)
+ it.numberOfRecommendations(numberOfRecommendations)
+ it.acceptedCharacterCount(acceptedCharCount)
+ }
}
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+ }
+
+ override fun sendUserTriggerDecisionTelemetry(
+ sessionContext: SessionContextNew,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ completionType: CodewhispererCompletionType,
+ suggestionState: CodewhispererSuggestionState,
+ suggestionReferenceCount: Int,
+ lineCount: Int,
+ numberOfRecommendations: Int,
+ acceptedCharCount: Int,
+ ): SendTelemetryEventResponse {
+ val fileContext = requestContext.fileContextInfo
+ val programmingLanguage = fileContext.programmingLanguage
+ var e2eLatency = sessionContext.latencyContext.getCodeWhispererEndToEndLatency()
// When we send a userTriggerDecision of Empty or Discard, we set the time users see the first
// suggestion to be now.
@@ -332,7 +378,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
it.sessionId(responseContext.sessionId)
it.recommendationLatencyMilliseconds(e2eLatency)
it.triggerToResponseLatencyMilliseconds(sessionContext.latencyContext.paginationFirstCompletionTime)
- it.perceivedLatencyMilliseconds(emittedPerceivedLatency)
+ it.perceivedLatencyMilliseconds(sessionContext.latencyContext.perceivedLatency)
it.suggestionState(suggestionState.toCodeWhispererSdkType())
it.timestamp(Instant.now())
it.suggestionReferenceCount(suggestionReferenceCount)
@@ -640,8 +686,6 @@ private fun CodewhispererSuggestionState.toCodeWhispererSdkType() = when {
this == CodewhispererSuggestionState.Reject -> SuggestionState.REJECT
this == CodewhispererSuggestionState.Empty -> SuggestionState.EMPTY
this == CodewhispererSuggestionState.Discard -> SuggestionState.DISCARD
- this == CodewhispererSuggestionState.Ignore -> SuggestionState.MERGE
- this == CodewhispererSuggestionState.Unseen -> SuggestionState.MERGE
else -> SuggestionState.UNKNOWN_TO_SDK_VERSION
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt
index 37c97107bf..323e66dfa4 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt
@@ -9,9 +9,11 @@ import com.intellij.openapi.editor.event.EditorFactoryEvent
import com.intellij.openapi.editor.event.EditorFactoryListener
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileEditor.FileDocumentManager
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker
class CodeWhispererEditorListener : EditorFactoryListener {
@@ -29,7 +31,11 @@ class CodeWhispererEditorListener : EditorFactoryListener {
// the most accurate code percentage data.
override fun documentChanged(event: DocumentEvent) {
if (!isCodeWhispererEnabled(project)) return
- CodeWhispererInvocationStatus.getInstance().documentChanged()
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererInvocationStatusNew.getInstance().documentChanged()
+ } else {
+ CodeWhispererInvocationStatus.getInstance().documentChanged()
+ }
CodeWhispererCodeCoverageTracker.getInstance(project, language).apply {
activateTrackerIfNotActive()
documentChanged(event)
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 8fc7efc725..22c8c2176b 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,21 +23,17 @@ import java.util.Stack
@Service
class CodeWhispererEditorManager {
- 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
+ fun updateEditorWithRecommendation(states: InvocationContext, sessionContext: SessionContext) {
+ val (requestContext, responseContext, recommendationContext) = states
+ val (project, editor) = requestContext
val document = editor.document
val primaryCaret = editor.caretModel.primaryCaret
- val typeahead = preview.typeahead
- val detail = preview.detail
- val userInput = preview.userInput
+ val selectedIndex = sessionContext.selectedIndex
+ val typeahead = sessionContext.typeahead
+ val detail = recommendationContext.details[selectedIndex]
val reformatted = CodeWhispererPopupManager.getInstance().getReformattedRecommendation(
detail,
- userInput
+ recommendationContext.userInputSinceInvocation
)
val remainingRecommendation = reformatted.substring(typeahead.length)
val originalOffset = primaryCaret.offset - typeahead.length
@@ -47,8 +43,6 @@ 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)
@@ -73,7 +67,7 @@ class CodeWhispererEditorManager {
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
- ).afterAccept(states, previews, sessionContext, rangeMarker)
+ ).afterAccept(states, sessionContext, rangeMarker)
}
}
}
@@ -123,9 +117,7 @@ class CodeWhispererEditorManager {
totalDocLengthChecked < lineText.length &&
totalDocLengthChecked < recommendation.length
) {
- var shouldContinue = true
val currentDocChar = lineText[totalDocLengthChecked]
-
if (!isMatchingSymbol(currentDocChar)) {
// currentDocChar is not a matching symbol, so we try to compare the remaining strings as a last step to match
val recommendationRemaining = recommendation.substring(current)
@@ -141,20 +133,19 @@ class CodeWhispererEditorManager {
totalDocLengthChecked++
// find symbol in the recommendation that will match this
- while (current < recommendation.length && shouldContinue) {
+ while (current < recommendation.length) {
val char = recommendation[current]
current++
// if char isn't a paired symbol, or it is, but it's not the matching currentDocChar or
// the opening version of it, then we're done
- if (!isMatchingSymbol(char) ||
- (char != currentDocChar && PAIRED_BRACKETS[char] != currentDocChar) ||
- PAIRED_BRACKETS[char] == currentDocChar
- ) {
- // if char is an opening bracket, push it to the stack
- if (PAIRED_BRACKETS[char] == currentDocChar) {
- bracketsStack.push(char)
- }
+ if (!isMatchingSymbol(char) || (char != currentDocChar && PAIRED_BRACKETS[char] != currentDocChar)) {
+ continue
+ }
+
+ // if char is an opening bracket, push it to the stack
+ if (PAIRED_BRACKETS[char] == currentDocChar) {
+ bracketsStack.push(char)
continue
}
@@ -164,13 +155,13 @@ class CodeWhispererEditorManager {
// on the stack
if (char.isWhitespace()) {
result.add(current to caretOffset + totalDocLengthChecked)
- shouldContinue = false
+ break
} else if (bracketsStack.isNotEmpty() && PAIRED_BRACKETS[bracketsStack.peek()] == char) {
bracketsStack.pop()
} else if (quotesStack.isNotEmpty() && quotesStack.peek().first == char) {
result.add(quotesStack.pop().second)
result.add(current to caretOffset + totalDocLengthChecked)
- shouldContinue = false
+ break
} else {
// char does not have a matching opening symbol in the stack, if it's a (opening) bracket,
// immediately add it to the result; if it's a quote, push it to the stack
@@ -179,7 +170,7 @@ class CodeWhispererEditorManager {
} else {
result.add(current to caretOffset + totalDocLengthChecked)
}
- shouldContinue = false
+ break
}
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt
new file mode 100644
index 0000000000..a244c31232
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt
@@ -0,0 +1,282 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.editor
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+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.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_QUOTES
+import java.time.Instant
+import java.util.Stack
+
+@Service
+class CodeWhispererEditorManagerNew {
+ fun updateEditorWithRecommendation(sessionContext: SessionContextNew) {
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ val selectedIndex = sessionContext.selectedIndex
+ val preview = previews[selectedIndex]
+ val states = CodeWhispererServiceNew.getInstance().getAllPaginationSessions()[preview.jobId] ?: return
+ val (requestContext, responseContext) = states
+ val (project, editor) = sessionContext
+ val document = editor.document
+ val primaryCaret = editor.caretModel.primaryCaret
+ val typeahead = preview.typeahead
+ val detail = preview.detail
+ val userInput = preview.userInput
+ val reformatted = CodeWhispererPopupManagerNew.getInstance().getReformattedRecommendation(
+ detail,
+ userInput
+ )
+ val remainingRecommendation = reformatted.substring(typeahead.length)
+ val originalOffset = primaryCaret.offset - typeahead.length
+
+ val endOffset = primaryCaret.offset + remainingRecommendation.length
+
+ 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)
+ primaryCaret.moveToOffset(endOffset + detail.rightOverlap.length)
+ }
+
+ ApplicationManager.getApplication().invokeLater {
+ WriteCommandAction.runWriteCommandAction(project) {
+ val rangeMarker = document.createRangeMarker(originalOffset, endOffset, true)
+
+ CodeWhispererTelemetryServiceNew.getInstance().enqueueAcceptedSuggestionEntry(
+ detail.requestId,
+ requestContext,
+ responseContext,
+ Instant.now(),
+ PsiDocumentManager.getInstance(project).getPsiFile(document)?.virtualFile,
+ rangeMarker,
+ remainingRecommendation,
+ selectedIndex,
+ detail.completionType
+ )
+
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
+ ).afterAccept(states, previews, sessionContext, rangeMarker)
+ }
+ }
+ }
+
+ private fun isMatchingSymbol(symbol: Char): Boolean =
+ PAIRED_BRACKETS.containsKey(symbol) || PAIRED_BRACKETS.containsValue(symbol) || PAIRED_QUOTES.contains(symbol) ||
+ symbol.isWhitespace()
+
+ fun getUserInputSinceInvocation(editor: Editor, invocationOffset: Int): String {
+ val currentOffset = editor.caretModel.primaryCaret.offset
+ return editor.document.getText(TextRange(invocationOffset, currentOffset))
+ }
+
+ fun getCaretMovement(editor: Editor, caretPosition: CaretPosition): CaretMovement {
+ val oldOffset = caretPosition.offset
+ val newOffset = editor.caretModel.primaryCaret.offset
+ return when {
+ oldOffset < newOffset -> CaretMovement.MOVE_FORWARD
+ oldOffset > newOffset -> CaretMovement.MOVE_BACKWARD
+ else -> CaretMovement.NO_CHANGE
+ }
+ }
+
+ fun getMatchingSymbolsFromRecommendation(
+ editor: Editor,
+ recommendation: String,
+ isTruncatedOnRight: Boolean,
+ sessionContext: SessionContextNew,
+ ): List> {
+ val result = mutableListOf>()
+ val bracketsStack = Stack()
+ val quotesStack = Stack>>()
+ val caretOffset = editor.caretModel.primaryCaret.offset
+ val document = editor.document
+ val lineEndOffset = document.getLineEndOffset(document.getLineNumber(caretOffset))
+ val lineText = document.charsSequence.subSequence(caretOffset, lineEndOffset)
+
+ var totalDocLengthChecked = 0
+ var current = 0
+ var shouldContinue = true
+
+ result.add(0 to caretOffset)
+ result.add(recommendation.length + 1 to lineEndOffset)
+
+ if (isTruncatedOnRight) return result
+
+ while (current < recommendation.length &&
+ totalDocLengthChecked < lineText.length &&
+ totalDocLengthChecked < recommendation.length
+ ) {
+ val currentDocChar = lineText[totalDocLengthChecked]
+ if (!isMatchingSymbol(currentDocChar)) {
+ // currentDocChar is not a matching symbol, so we try to compare the remaining strings as a last step to match
+ val recommendationRemaining = recommendation.substring(current)
+ val rightContextRemaining = lineText.subSequence(totalDocLengthChecked, lineText.length).toString()
+ if (recommendationRemaining == rightContextRemaining) {
+ for (i in 1..recommendation.length - current) {
+ result.add(current + i to caretOffset + totalDocLengthChecked + i)
+ }
+ result.sortBy { it.first }
+ }
+ break
+ }
+ totalDocLengthChecked++
+
+ // find symbol in the recommendation that will match this
+ while (current < recommendation.length && shouldContinue) {
+ val char = recommendation[current]
+ current++
+
+ // if char isn't a paired symbol, or it is, but it's not the matching currentDocChar or
+ // the opening version of it, then we're done
+ if (!isMatchingSymbol(char) ||
+ (char != currentDocChar && PAIRED_BRACKETS[char] != currentDocChar) ||
+ PAIRED_BRACKETS[char] == currentDocChar
+ ) {
+ // if char is an opening bracket, push it to the stack
+ if (PAIRED_BRACKETS[char] == currentDocChar) {
+ bracketsStack.push(char)
+ }
+ continue
+ }
+
+ // char is currentDocChar, it's one of a bracket, a quote, or a whitespace character.
+ // If it's a whitespace character, directly add it to the result,
+ // if it's a bracket or a quote, check if this char is already having a matching opening symbol
+ // on the stack
+ if (char.isWhitespace()) {
+ result.add(current to caretOffset + totalDocLengthChecked)
+ shouldContinue = false
+ } else if (bracketsStack.isNotEmpty() && PAIRED_BRACKETS[bracketsStack.peek()] == char) {
+ bracketsStack.pop()
+ } else if (quotesStack.isNotEmpty() && quotesStack.peek().first == char) {
+ result.add(quotesStack.pop().second)
+ result.add(current to caretOffset + totalDocLengthChecked)
+ shouldContinue = false
+ } else {
+ // char does not have a matching opening symbol in the stack, if it's a (opening) bracket,
+ // immediately add it to the result; if it's a quote, push it to the stack
+ if (PAIRED_QUOTES.contains(char)) {
+ quotesStack.push(char to (current to caretOffset + totalDocLengthChecked))
+ } else {
+ result.add(current to caretOffset + totalDocLengthChecked)
+ }
+ shouldContinue = false
+ }
+ }
+ }
+
+ // if there are any symbols left in the stack, add them to the result
+ quotesStack.forEach { result.add(it.second) }
+ result.sortBy { it.first }
+
+ sessionContext.insertEndOffset = result[result.size - 2].second
+
+ return result
+ }
+
+ // example: recommendation: document
+ // line1
+ // line2
+ // line3 line3
+ // line4
+ // ...
+ // number of lines overlapping would be one, and it will be line 3
+ fun findOverLappingLines(
+ editor: Editor,
+ recommendationLines: List,
+ isTruncatedOnRight: Boolean,
+ sessionContext: SessionContextNew,
+ ): Int {
+ val caretOffset = editor.caretModel.offset
+ if (isTruncatedOnRight) {
+ // insertEndOffset value only makes sense when there are matching closing brackets, if there's right context
+ // resolution applied, set this value to the current caret offset
+ sessionContext.insertEndOffset = caretOffset
+ return 0
+ }
+
+ val text = editor.document.charsSequence
+ val document = editor.document
+ val textLines = mutableListOf>()
+ val caretLine = document.getLineNumber(caretOffset)
+ var currentLineNum = caretLine + 1
+ val recommendationLinesNotBlank = recommendationLines.filter { it.isNotBlank() }
+ while (currentLineNum < document.lineCount && textLines.size < recommendationLinesNotBlank.size) {
+ val currentLine = text.subSequence(
+ document.getLineStartOffset(currentLineNum),
+ document.getLineEndOffset(currentLineNum)
+ )
+ if (currentLine.isNotBlank()) {
+ textLines.add(currentLine.toString() to document.getLineEndOffset(currentLineNum))
+ }
+ currentLineNum++
+ }
+
+ val numOfNonEmptyLinesMatching = countNonEmptyLinesMatching(recommendationLinesNotBlank, textLines)
+ val numOfLinesMatching = countLinesMatching(recommendationLines, numOfNonEmptyLinesMatching)
+ if (numOfNonEmptyLinesMatching > 0) {
+ sessionContext.insertEndOffset = textLines[numOfNonEmptyLinesMatching - 1].second
+ } else if (recommendationLines.isNotEmpty()) {
+ sessionContext.insertEndOffset = document.getLineEndOffset(caretLine)
+ }
+
+ return numOfLinesMatching
+ }
+
+ private fun countLinesMatching(lines: List, targetNonEmptyLines: Int): Int {
+ var count = 0
+ var nonEmptyCount = 0
+
+ for (line in lines.asReversed()) {
+ if (nonEmptyCount == targetNonEmptyLines) {
+ break
+ }
+ if (line.isNotBlank()) {
+ nonEmptyCount++
+ }
+ count++
+ }
+ return count
+ }
+
+ private fun countNonEmptyLinesMatching(recommendationLines: List, textLines: List>): Int {
+ // i lines we want to match
+ for (i in textLines.size downTo 1) {
+ val recommendationStart = recommendationLines.size - i
+ var matching = true
+ for (j in 0 until i) {
+ if (recommendationLines[recommendationStart + j].trimEnd() != textLines[j].first.trimEnd()) {
+ matching = false
+ break
+ }
+ }
+ if (matching) {
+ return i
+ }
+ }
+ return 0
+ }
+
+ companion object {
+ fun getInstance(): CodeWhispererEditorManagerNew = service()
+ }
+}
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 6c3bd789c2..0d4f287372 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,14 +14,25 @@ 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.InvocationContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
abstract class CodeWhispererImportAdder {
abstract val supportedLanguages: List
abstract val dummyFileName: String
- fun insertImportStatements(states: InvocationContext, previews: List, sessionContext: SessionContext) {
+ fun insertImportStatements(states: InvocationContext, sessionContext: SessionContext) {
+ val imports = states.recommendationContext.details[sessionContext.selectedIndex]
+ .recommendation.mostRelevantMissingImports()
+ LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" }
+ imports.forEach {
+ insertImportStatement(states, it)
+ }
+ }
+
+ fun insertImportStatements(states: InvocationContextNew, previews: List, sessionContext: SessionContextNew) {
val imports = previews[sessionContext.selectedIndex].detail.recommendation.mostRelevantMissingImports()
LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" }
imports.forEach {
@@ -61,6 +72,38 @@ abstract class CodeWhispererImportAdder {
LOG.info { "Added import: $added" }
}
+ private fun insertImportStatement(states: InvocationContextNew, import: Import) {
+ val project = states.requestContext.project
+ val editor = states.requestContext.editor
+ val document = editor.document
+ val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return
+
+ val statement = import.statement()
+ LOG.info { "Import statement to be added: $statement" }
+ val newImport = createNewImportPsiElement(psiFile, statement)
+ if (newImport == null) {
+ LOG.debug { "Failed to create the import element using the import string" }
+ return
+ }
+
+ if (!isSupportedImportStyle(newImport)) {
+ LOG.debug { "Import statement \"${newImport.text}\" is not supported" }
+ return
+ }
+
+ LOG.debug { "Checking duplicates with existing imports" }
+ val hasDuplicate = hasDuplicatedImports(psiFile, editor, newImport)
+ if (hasDuplicate) {
+ LOG.debug { "Found duplicates with existing imports, not adding the new import" }
+ return
+ } else {
+ LOG.debug { "Found no duplicates with existing imports" }
+ }
+
+ val added = addImport(psiFile, editor, newImport)
+ LOG.info { "Added import: $added" }
+ }
+
abstract fun createNewImportPsiElement(psiFile: PsiFile, statement: String): PsiElement?
open fun isSupportedImportStyle(newImport: PsiElement) = true
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 8dbf816823..a5abde5d9b 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,14 +7,34 @@ 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.InvocationContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
object CodeWhispererImportAdderListener : CodeWhispererUserActionListener {
internal val LOG = getLogger()
- override fun afterAccept(states: InvocationContext, previews: List, sessionContext: SessionContext, rangeMarker: RangeMarker) {
+ override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {
+ if (!CodeWhispererSettings.getInstance().isImportAdderEnabled()) {
+ LOG.debug { "Import adder not enabled in user settings" }
+ return
+ }
+ val language = states.requestContext.fileContextInfo.programmingLanguage
+ if (!language.isImportAdderSupported()) {
+ LOG.debug { "Import adder is not supported for $language" }
+ return
+ }
+ val importAdder = CodeWhispererImportAdder.get(language)
+ if (importAdder == null) {
+ LOG.debug { "No import adder found for $language" }
+ return
+ }
+ importAdder.insertImportStatements(states, sessionContext)
+ }
+
+ override fun afterAccept(states: InvocationContextNew, previews: List, sessionContext: SessionContextNew, rangeMarker: RangeMarker) {
if (!CodeWhispererSettings.getInstance().isImportAdderEnabled()) {
LOG.debug { "Import adder not enabled in user settings" }
return
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
new file mode 100644
index 0000000000..e72269ce2f
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt
@@ -0,0 +1,92 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.inlay
+
+import com.intellij.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
+
+@Service
+class CodeWhispererInlayManager {
+ private val existingInlays = mutableListOf>()
+ fun updateInlays(states: InvocationContext, chunks: List) {
+ val editor = states.requestContext.editor
+ clearInlays()
+
+ chunks.forEach { chunk ->
+ createCodeWhispererInlays(editor, chunk.inlayOffset, chunk.text, states.popup)
+ }
+ }
+
+ private fun createCodeWhispererInlays(editor: Editor, startOffset: Int, inlayText: String, popup: JBPopup) {
+ if (inlayText.isEmpty()) return
+ val firstNewlineIndex = inlayText.indexOf("\n")
+ val firstLine: String
+ val otherLines: String
+ if (firstNewlineIndex != -1 && firstNewlineIndex < inlayText.length - 1) {
+ firstLine = inlayText.substring(0, firstNewlineIndex)
+ otherLines = inlayText.substring(firstNewlineIndex + 1)
+ } else {
+ firstLine = inlayText
+ otherLines = ""
+ }
+
+ if (firstLine.isNotEmpty()) {
+ val firstLineRenderer =
+ if (!AppMode.isRemoteDevHost()) {
+ CodeWhispererInlayInlineRenderer(firstLine)
+ } else {
+ InlineCompletionRemoteRendererFactory.createLineInlay(editor, firstLine)
+ }
+ val inlineInlay = editor.inlayModel.addInlineElement(startOffset, true, firstLineRenderer)
+ inlineInlay?.let {
+ existingInlays.add(it)
+ Disposer.register(popup, it)
+ }
+ }
+
+ if (otherLines.isEmpty()) {
+ return
+ }
+ val otherLinesRenderers =
+ if (!AppMode.isRemoteDevHost()) {
+ listOf(CodeWhispererInlayBlockRenderer(otherLines))
+ } else {
+ InlineCompletionRemoteRendererFactory.createBlockInlays(editor, otherLines.split("\n"))
+ }
+
+ otherLinesRenderers.forEach { otherLinesRenderer ->
+ val blockInlay = editor.inlayModel.addBlockElement(
+ startOffset,
+ true,
+ false,
+ 0,
+ otherLinesRenderer
+ )
+ blockInlay?.let {
+ existingInlays.add(it)
+ Disposer.register(popup, it)
+ }
+ }
+ }
+
+ fun clearInlays() {
+ existingInlays.forEach {
+ Disposer.dispose(it)
+ }
+ existingInlays.clear()
+ }
+
+ companion object {
+ @JvmStatic
+ fun getInstance(): CodeWhispererInlayManager = service()
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManagerNew.kt
index dad18f27a4..339b5aa8df 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManagerNew.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManagerNew.kt
@@ -10,12 +10,12 @@ import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.util.Disposer
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.model.SessionContextNew
@Service
class CodeWhispererInlayManagerNew {
private val existingInlays = mutableListOf>()
- fun updateInlays(sessionContext: SessionContext, chunks: List) {
+ fun updateInlays(sessionContext: SessionContextNew, chunks: List) {
clearInlays()
chunks.forEach { chunk ->
@@ -23,7 +23,7 @@ class CodeWhispererInlayManagerNew {
}
}
- private fun createCodeWhispererInlays(sessionContext: SessionContext, startOffset: Int, inlayText: String) {
+ private fun createCodeWhispererInlays(sessionContext: SessionContextNew, startOffset: Int, inlayText: String) {
if (inlayText.isEmpty()) return
val editor = sessionContext.editor
val firstNewlineIndex = inlayText.indexOf("\n")
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 92438d225d..91da779ab1 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
@@ -12,20 +12,22 @@ 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 kotlinx.coroutines.Deferred
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT
import software.aws.toolkits.jetbrains.services.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.popup.CodeWhispererPopupManagerNew
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.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.telemetry.CodeWhispererTelemetryService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.setIntelliSensePopupAlpha
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy
@@ -94,7 +96,14 @@ data class SupplementalContextInfo(
}
data class RecommendationContext(
- val details: MutableList,
+ val details: List,
+ val userInputOriginal: String,
+ val userInputSinceInvocation: String,
+ val position: VisualPosition,
+)
+
+data class RecommendationContextNew(
+ val details: MutableList,
val userInputOriginal: String,
val userInputSinceInvocation: String,
val position: VisualPosition,
@@ -104,7 +113,7 @@ data class RecommendationContext(
data class PreviewContext(
val jobId: Int,
- val detail: DetailContext,
+ val detail: DetailContextNew,
val userInput: String,
val typeahead: String,
)
@@ -117,11 +126,31 @@ data class DetailContext(
val isTruncatedOnRight: Boolean,
val rightOverlap: String = "",
val completionType: CodewhispererCompletionType,
+)
+
+data class DetailContextNew(
+ val requestId: String,
+ val recommendation: Completion,
+ val reformatted: Completion,
+ val isDiscarded: Boolean,
+ val isTruncatedOnRight: Boolean,
+ val rightOverlap: String = "",
+ val completionType: CodewhispererCompletionType,
var hasSeen: Boolean = false,
var isAccepted: Boolean = false,
)
data class SessionContext(
+ val typeahead: String = "",
+ val typeaheadOriginal: String = "",
+ val selectedIndex: Int = 0,
+ val seen: MutableSet = mutableSetOf(),
+ val isFirstTimeShowingPopup: Boolean = true,
+ var toBeRemovedHighlighter: RangeHighlighter? = null,
+ var insertEndOffset: Int = -1,
+)
+
+data class SessionContextNew(
val project: Project,
val editor: Editor,
var popup: JBPopup? = null,
@@ -136,10 +165,10 @@ data class SessionContext(
private var isDisposed = false
init {
project.messageBus.connect(this).subscribe(
- CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER,
+ CodeWhispererServiceNew.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER,
object : CodeWhispererIntelliSenseOnHoverListener {
override fun onEnter() {
- CodeWhispererPopupManager.getInstance().bringSuggestionInlayToFront(editor, popup, opposite = true)
+ CodeWhispererPopupManagerNew.getInstance().bringSuggestionInlayToFront(editor, popup, opposite = true)
}
}
)
@@ -147,13 +176,13 @@ data class SessionContext(
@RequiresEdt
override fun dispose() {
- CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
+ CodeWhispererTelemetryServiceNew.getInstance().sendUserDecisionEventForAll(
this,
hasAccepted,
- CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
+ CodeWhispererInvocationStatusNew.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
)
setIntelliSensePopupAlpha(editor, 0f)
- CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false)
+ CodeWhispererInvocationStatusNew.getInstance().setDisplaySessionActive(false)
if (hasAccepted) {
popup?.closeOk(null)
@@ -162,7 +191,7 @@ data class SessionContext(
}
popup?.let { Disposer.dispose(it) }
popup = null
- CodeWhispererInvocationStatus.getInstance().finishInvocation()
+ CodeWhispererInvocationStatusNew.getInstance().finishInvocation()
isDisposed = true
}
@@ -186,6 +215,15 @@ data class InvocationContext(
val requestContext: RequestContext,
val responseContext: ResponseContext,
val recommendationContext: RecommendationContext,
+ val popup: JBPopup,
+) : Disposable {
+ override fun dispose() {}
+}
+
+data class InvocationContextNew(
+ val requestContext: RequestContextNew,
+ val responseContext: ResponseContext,
+ val recommendationContext: RecommendationContextNew,
) : Disposable {
private var isDisposed = false
@@ -196,11 +234,17 @@ data class InvocationContext(
fun isDisposed() = isDisposed
}
-
data class WorkerContext(
val requestContext: RequestContext,
val responseContext: ResponseContext,
val response: GenerateCompletionsResponse,
+ val popup: JBPopup,
+)
+
+data class WorkerContextNew(
+ val requestContext: RequestContextNew,
+ val responseContext: ResponseContext,
+ val response: GenerateCompletionsResponse,
)
data class CodeScanTelemetryEvent(
@@ -283,37 +327,3 @@ data class TryExampleRowContext(
val description: String,
val filename: String?,
)
-
-data class RequestContext(
- val project: Project,
- val editor: Editor,
- val triggerTypeInfo: TriggerTypeInfo,
- val caretPosition: CaretPosition,
- val fileContextInfo: FileContextInfo,
- private val supplementalContextDeferred: Deferred,
- val connection: ToolkitConnection?,
- val customizationArn: String?,
-) {
- // TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only
- var supplementalContext: SupplementalContextInfo? = null
- private set
- get() = when (field) {
- null -> {
- if (!supplementalContextDeferred.isCompleted) {
- error("attempt to access supplemental context before awaiting the deferred")
- } else {
- null
- }
- }
- else -> field
- }
-
- suspend fun awaitSupplementalContext(): SupplementalContextInfo? {
- supplementalContext = supplementalContextDeferred.await()
- return supplementalContext
- }
-}
-
-data class ResponseContext(
- val sessionId: String,
-)
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 8d4e288b5f..145e3cf624 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,11 +5,38 @@ 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.service.CodeWhispererService
+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.service.CodeWhispererServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
+import java.time.Duration
+import java.time.Instant
-class CodeWhispererPopupListener : JBPopupListener {
+class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopupListener {
+ override fun beforeShown(event: LightweightWindowEvent) {
+ super.beforeShown(event)
+ CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp()
+ }
+ 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)
+ }
+}
+
+class CodeWhispererPopupListenerNew : JBPopupListener {
override fun onClosed(event: LightweightWindowEvent) {
super.onClosed(event)
- CodeWhispererService.getInstance().disposeDisplaySession(event.isOk)
+ CodeWhispererServiceNew.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 60482a47e7..29ffb766a8 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,11 +3,14 @@
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.IdeActions.ACTION_EDITOR_BACKSPACE
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
-import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ESCAPE
+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.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.Service
@@ -24,10 +27,9 @@ 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
@@ -37,7 +39,6 @@ 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
@@ -50,12 +51,16 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhisper
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.InvocationContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.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.CodeWhispererPopupEscHandler
+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.CodeWhispererPopupTypedHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererAcceptButtonActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererActionListener
@@ -63,7 +68,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.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE
@@ -79,10 +84,14 @@ import javax.swing.JLabel
@Service
class CodeWhispererPopupManager {
- val popupComponents = CodeWhispererPopupComponentsNew()
+ val popupComponents = CodeWhispererPopupComponents()
var shouldListenerCancelPopup: Boolean = true
private set
+ var sessionContext = SessionContext()
+ private set
+
+ private var myPopup: JBPopup? = null
init {
// Listen for global scheme changes
@@ -109,106 +118,113 @@ class CodeWhispererPopupManager {
)
}
- @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 isReverse = indexChange < 0
- val selectedIndex = findNewSelectedIndex(isReverse, sessionContext.selectedIndex + indexChange)
-
- sessionContext.selectedIndex = selectedIndex
-
- ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
- sessionContext
- )
- }
-
- @RequiresEdt
- fun changeStatesForTypeahead(
- sessionContext: SessionContext,
+ fun changeStates(
+ states: InvocationContext,
+ indexChange: Int,
typeaheadChange: String,
typeaheadAdded: Boolean,
+ recommendationAdded: Boolean = false,
) {
- if (!updateTypeahead(typeaheadChange, typeaheadAdded)) return
- if (!updateSessionSelectedIndex(sessionContext)) return
-
- ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
- sessionContext
- )
- }
-
- @RequiresEdt
- fun changeStatesForShowing(sessionContext: SessionContext, states: InvocationContext, 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
}
-
- if (!updateSessionSelectedIndex(sessionContext)) return
- if (sessionContext.popupOffset == -1) {
- sessionContext.popupOffset = sessionContext.editor.caretModel.offset
+ 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)
+ if ((validSelectedIndex == validCount - 1 && indexChange == 1) ||
+ (validSelectedIndex == 0 && indexChange == -1)
+ ) {
+ return
}
+ val selectedIndex = findNewSelectedIndex(
+ isReverse,
+ details,
+ userInput,
+ sessionContext.selectedIndex + indexChange,
+ typeaheadOriginal
+ )
+ if (selectedIndex == -1 || !isValidRecommendation(details[selectedIndex], userInput, typeaheadOriginal)) {
+ LOG.debug { "None of the recommendation is valid at this point, cancelling the popup" }
+ cancelPopup(popup)
+ return
+ }
+ val typeahead = resolveTypeahead(states, selectedIndex, typeaheadOriginal)
+ val isFirstTimeShowingPopup = indexChange == 0 && typeaheadChange.isEmpty()
+ sessionContext = SessionContext(
+ typeahead,
+ typeaheadOriginal,
+ selectedIndex,
+ sessionContext.seen,
+ isFirstTimeShowingPopup,
+ sessionContext.toBeRemovedHighlighter
+ )
ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
+ states,
sessionContext
)
}
- private fun updateTypeahead(typeaheadChange: String, typeaheadAdded: Boolean): Boolean {
- val recommendations = CodeWhispererService.getInstance().getAllPaginationSessions().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" }
- CodeWhispererService.getInstance().disposeDisplaySession(false)
- return false
- }
- it.recommendationContext.typeahead.substring(
- 0,
- it.recommendationContext.typeahead.length - typeaheadChange.length
- )
- }
- it.recommendationContext.typeahead = newTypeahead
+ 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
}
- return true
- }
- private fun updateSessionSelectedIndex(sessionContext: SessionContext): Boolean {
- 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 false
+ for (i in 0..indexOfFirstNonWhiteSpace) {
+ val subTypeahead = typeahead.substring(i)
+ if (recommendation.startsWith(userInput + subTypeahead)) return subTypeahead
}
-
- sessionContext.selectedIndex = selectedIndex
- return true
+ return typeahead
}
- fun updatePopupPanel(sessionContext: SessionContext?) {
- if (sessionContext == null || sessionContext.selectedIndex == -1 || sessionContext.isDisposed()) return
+ fun updatePopupPanel(states: InvocationContext, sessionContext: SessionContext) {
+ val userInput = states.recommendationContext.userInputSinceInvocation
+ val details = states.recommendationContext.details
val selectedIndex = sessionContext.selectedIndex
- val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
- if (selectedIndex >= previews.size) return
- val validCount = getValidCount()
- val validSelectedIndex = getValidSelectedIndex(selectedIndex)
+ val typeaheadOriginal = sessionContext.typeaheadOriginal
+ val validCount = getValidCount(details, userInput, typeaheadOriginal)
+ val validSelectedIndex = getValidSelectedIndex(details, userInput, selectedIndex, typeaheadOriginal)
updateSelectedRecommendationLabelText(validSelectedIndex, validCount)
updateNavigationPanel(validSelectedIndex, validCount)
- updateImportPanel(previews[selectedIndex].detail.recommendation.mostRelevantMissingImports())
- updateCodeReferencePanel(sessionContext.project, previews[selectedIndex].detail.recommendation.references())
+ updateImportPanel(details[selectedIndex].recommendation.mostRelevantMissingImports())
+ updateCodeReferencePanel(states.requestContext.project, details[selectedIndex].recommendation.references())
}
- fun render(sessionContext: SessionContext, isRecommendationAdded: Boolean) {
- updatePopupPanel(sessionContext)
+ 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)
// 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
@@ -219,16 +235,22 @@ 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 (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) {
- sessionContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime()
- sessionContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime()
- val triggerTypeOfLastTrigger = CodeWhispererService.getInstance().getAllPaginationSessions()
- .values.filterNotNull().last().requestContext.triggerTypeInfo.triggerType
- sessionContext.latencyContext.perceivedLatency =
- sessionContext.latencyContext.getPerceivedLatency(triggerTypeOfLastTrigger)
+ if (!CodeWhispererInvocationStatus.getInstance().isPopupActive()) {
+ states.requestContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime()
+ states.requestContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime()
+ states.requestContext.latencyContext.perceivedLatency =
+ states.requestContext.latencyContext.getPerceivedLatency(states.requestContext.triggerTypeInfo.triggerType)
+ }
+ if (!isRecommendationAdded) {
+ showPopup(states, sessionContext, states.popup, caretPoint, overlappingLinesCount)
}
- if (isRecommendationAdded) return
- showPopup(sessionContext)
+ if (isScrolling ||
+ CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation() ||
+ !sessionContext.isFirstTimeShowingPopup
+ ) {
+ return
+ }
+ CodeWhispererTelemetryService.getInstance().sendClientComponentLatencyEvent(states)
}
fun dontClosePopupAndRun(runnable: () -> Unit) {
@@ -240,36 +262,84 @@ class CodeWhispererPopupManager {
}
}
- fun showPopup(sessionContext: SessionContext, force: Boolean = false) {
- val p = sessionContext.editor.offsetToXY(sessionContext.popupOffset)
- val popup: JBPopup?
- if (sessionContext.popup == null) {
- popup = initPopup()
- sessionContext.popup = popup
- CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp()
- initPopupListener(sessionContext, popup)
- } else {
- popup = sessionContext.popup
+ 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)
}
- val editor = sessionContext.editor
- val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
- val userInputOriginal = previews[sessionContext.selectedIndex].userInput
+ }
+
+ 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 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 yAboveFirstLine = p.y - popupSize.height + userInputLines * editor.lineHeight
- val popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height)
+ val yBelowLastLine = p.y + (lineCount + additionalLines + userInputLines - overlappingLinesCount) * editor.lineHeight
+ val yAboveFirstLine = p.y - popupSize.height + (additionalLines + userInputLines) * editor.lineHeight
val editorRect = editor.scrollingModel.visibleArea
+ var popupRect = Rectangle(p.x, yBelowLastLine, popupSize.width, popupSize.height)
var shouldHidePopup = false
- CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true)
+ CodeWhispererInvocationStatus.getInstance().setPopupActive(true)
- if (!editorRect.contains(popupRect)) {
- // popup location above first line don't work, so don't show the popup
- shouldHidePopup = 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
}
- // popup to always display above the current editing line
- val popupLocation = Point(p.x, yAboveFirstLine)
+ 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)
+ }
val relativePopupLocationToEditor = RelativePoint(editor.contentComponent, popupLocation)
@@ -282,11 +352,8 @@ class CodeWhispererPopupManager {
}
} else {
if (!AppMode.isRemoteDevHost()) {
- if (force && !shouldHidePopup) {
- popup.show(relativePopupLocationToEditor)
- }
+ 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
@@ -301,20 +368,22 @@ 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
+ )
}
- 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)
+ // 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)
+ }
}
}
@@ -322,190 +391,162 @@ class CodeWhispererPopupManager {
.createComponentPopupBuilder(popupComponents.panel, null)
.setAlpha(0.1F)
.setCancelOnClickOutside(true)
+ .setCancelOnOtherWindowOpen(true)
+ .setCancelKeyEnabled(true)
.setCancelOnWindowDeactivation(true)
- .createPopup()
+ .createPopup().also {
+ myPopup = it
+ }
fun getReformattedRecommendation(detailContext: DetailContext, userInput: String) =
detailContext.reformatted.content().substring(userInput.length)
- private fun initPopupListener(sessionContext: SessionContext, popup: JBPopup) {
- addPopupListener(popup)
- sessionContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListener(sessionContext), sessionContext)
- addButtonActionListeners(sessionContext)
- addMessageSubscribers(sessionContext)
- setPopupActionHandlers(sessionContext)
- addComponentListeners(sessionContext)
+ fun initPopupListener(states: InvocationContext) {
+ addPopupListener(states)
+ states.requestContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListener(states), states)
+ addButtonActionListeners(states)
+ addMessageSubscribers(states)
+ setPopupActionHandlers(states)
+ addComponentListeners(states)
}
- private fun addPopupListener(popup: JBPopup) {
- val listener = CodeWhispererPopupListener()
- popup.addListener(listener)
- Disposer.register(popup) {
- popup.removeListener(listener)
- }
+ private fun addPopupListener(states: InvocationContext) {
+ val listener = CodeWhispererPopupListener(states)
+ states.popup.addListener(listener)
+ Disposer.register(states) { states.popup.removeListener(listener) }
}
- private fun addMessageSubscribers(sessionContext: SessionContext) {
- val connect = ApplicationManager.getApplication().messageBus.connect(sessionContext)
+ private fun addMessageSubscribers(states: InvocationContext) {
+ val connect = ApplicationManager.getApplication().messageBus.connect(states)
connect.subscribe(
CODEWHISPERER_USER_ACTION_PERFORMED,
object : CodeWhispererUserActionListener {
- override fun navigateNext(sessionContext: SessionContext) {
- changeStatesForNavigation(sessionContext, 1)
+ override fun navigateNext(states: InvocationContext) {
+ changeStates(states, 1, "", true)
}
- override fun navigatePrevious(sessionContext: SessionContext) {
- changeStatesForNavigation(sessionContext, -1)
+ override fun navigatePrevious(states: InvocationContext) {
+ changeStates(states, -1, "", true)
}
- override fun backspace(sessionContext: SessionContext, diff: String) {
- changeStatesForTypeahead(sessionContext, diff, false)
+ override fun backspace(states: InvocationContext, diff: String) {
+ changeStates(states, 0, diff, false)
}
- override fun enter(sessionContext: SessionContext, diff: String) {
- changeStatesForTypeahead(sessionContext, diff, true)
+ override fun enter(states: InvocationContext, diff: String) {
+ changeStates(states, 0, diff, true)
}
- override fun type(sessionContext: SessionContext, diff: String) {
+ override fun type(states: InvocationContext, diff: String) {
// remove the character at primaryCaret if it's the same as the typed character
- val caretOffset = sessionContext.editor.caretModel.primaryCaret.offset
- val document = sessionContext.editor.document
+ val caretOffset = states.requestContext.editor.caretModel.primaryCaret.offset
+ val document = states.requestContext.editor.document
val text = document.charsSequence
if (caretOffset < text.length && diff == text[caretOffset].toString()) {
- WriteCommandAction.runWriteCommandAction(sessionContext.project) {
+ WriteCommandAction.runWriteCommandAction(states.requestContext.project) {
document.deleteString(caretOffset, caretOffset + 1)
}
}
- changeStatesForTypeahead(sessionContext, diff, true)
+ changeStates(states, 0, diff, true)
}
- override fun beforeAccept(sessionContext: SessionContext) {
+ override fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) {
dontClosePopupAndRun {
- CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(sessionContext)
+ CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(states, sessionContext)
}
- CodeWhispererService.getInstance().disposeDisplaySession(true)
+ closePopup(states.popup)
}
}
)
}
- 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 addButtonActionListeners(states: InvocationContext) {
+ popupComponents.prevButton.addButtonActionListener(CodeWhispererPrevButtonActionListener(states))
+ popupComponents.nextButton.addButtonActionListener(CodeWhispererNextButtonActionListener(states))
+ popupComponents.acceptButton.addButtonActionListener(CodeWhispererAcceptButtonActionListener(states))
}
- private fun JButton.addButtonActionListener(listener: CodeWhispererActionListener, sessionContext: SessionContext) {
+ private fun JButton.addButtonActionListener(listener: CodeWhispererActionListener) {
this.addActionListener(listener)
- Disposer.register(sessionContext) { this.removeActionListener(listener) }
+ Disposer.register(listener.states) { this.removeActionListener(listener) }
}
- private fun setPopupActionHandlers(sessionContext: SessionContext) {
+ private fun setPopupActionHandlers(states: InvocationContext) {
val actionManager = EditorActionManager.getInstance()
-
- sessionContext.project.putUserData(CodeWhispererService.KEY_SESSION_CONTEXT, sessionContext)
-
- setPopupTypedHandler(CodeWhispererPopupTypedHandler(TypedAction.getInstance().rawHandler, sessionContext), sessionContext)
- setPopupActionHandler(ACTION_EDITOR_ESCAPE, CodeWhispererPopupEscHandler(sessionContext), sessionContext)
+ 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))
setPopupActionHandler(
ACTION_EDITOR_ENTER,
- CodeWhispererPopupEnterHandler(actionManager.getActionHandler(ACTION_EDITOR_ENTER), sessionContext),
- sessionContext
+ CodeWhispererPopupEnterHandler(actionManager.getActionHandler(ACTION_EDITOR_ENTER), states)
)
setPopupActionHandler(
ACTION_EDITOR_BACKSPACE,
- CodeWhispererPopupBackspaceHandler(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), sessionContext),
- sessionContext
+ CodeWhispererPopupBackspaceHandler(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), states)
)
}
- private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandler, sessionContext: SessionContext) {
+ private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandler) {
val oldTypedHandler = TypedAction.getInstance().setupRawHandler(newHandler)
- Disposer.register(sessionContext) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) }
+ Disposer.register(newHandler.states) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) }
}
- private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandler, sessionContext: SessionContext) {
+ private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandler) {
val oldHandler = EditorActionManager.getInstance().setActionHandler(id, newHandler)
- Disposer.register(sessionContext) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) }
+ Disposer.register(newHandler.states) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) }
}
- private fun addComponentListeners(sessionContext: SessionContext) {
- val editor = sessionContext.editor
- val codeWhispererSelectionListener: SelectionListener = object : SelectionListener {
+ private fun addComponentListeners(states: InvocationContext) {
+ val editor = states.requestContext.editor
+ val codewhispererSelectionListener: SelectionListener = object : SelectionListener {
override fun selectionChanged(event: SelectionEvent) {
if (shouldListenerCancelPopup) {
- CodeWhispererService.getInstance().disposeDisplaySession(false)
+ cancelPopup(states.popup)
}
super.selectionChanged(event)
}
}
- editor.selectionModel.addSelectionListener(codeWhispererSelectionListener)
- Disposer.register(sessionContext) { editor.selectionModel.removeSelectionListener(codeWhispererSelectionListener) }
+ editor.selectionModel.addSelectionListener(codewhispererSelectionListener)
+ Disposer.register(states) { editor.selectionModel.removeSelectionListener(codewhispererSelectionListener) }
- val codeWhispererDocumentListener: DocumentListener = object : DocumentListener {
+ val codewhispererDocumentListener: DocumentListener = object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
if (shouldListenerCancelPopup) {
- // 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)
- }
+ cancelPopup(states.popup)
}
super.documentChanged(event)
}
}
- editor.document.addDocumentListener(codeWhispererDocumentListener, sessionContext)
+ editor.document.addDocumentListener(codewhispererDocumentListener, states)
- val codeWhispererCaretListener: CaretListener = object : CaretListener {
+ val codewhispererCaretListener: CaretListener = object : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {
if (shouldListenerCancelPopup) {
- CodeWhispererService.getInstance().disposeDisplaySession(false)
+ cancelPopup(states.popup)
}
super.caretPositionChanged(event)
}
}
- editor.caretModel.addCaretListener(codeWhispererCaretListener)
- Disposer.register(sessionContext) { editor.caretModel.removeCaretListener(codeWhispererCaretListener) }
+ editor.caretModel.addCaretListener(codewhispererCaretListener)
+ Disposer.register(states) { editor.caretModel.removeCaretListener(codewhispererCaretListener) }
val editorComponent = editor.contentComponent
if (editorComponent.isShowing) {
val window = ComponentUtil.getWindow(editorComponent)
val windowListener: ComponentListener = object : ComponentAdapter() {
- override fun componentMoved(e: ComponentEvent) {
- CodeWhispererService.getInstance().disposeDisplaySession(false)
- super.componentMoved(e)
+ override fun componentMoved(event: ComponentEvent) {
+ cancelPopup(states.popup)
}
override fun componentShown(e: ComponentEvent?) {
- CodeWhispererService.getInstance().disposeDisplaySession(false)
+ cancelPopup(states.popup)
super.componentShown(e)
}
}
window?.addComponentListener(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 {
- bringSuggestionInlayToFront(sessionContext.editor, sessionContext.popup, opposite = true)
- }
- super.mouseMoved(e)
- }
+ Disposer.register(states) { window?.removeComponentListener(windowListener) }
}
- editor.addEditorMouseMotionListener(suggestionHoverEnterListener, sessionContext)
}
private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) {
@@ -582,10 +623,18 @@ class CodeWhispererPopupManager {
}
}
- fun findNewSelectedIndex(isReverse: Boolean, selectedIndex: Int): Int {
- val start = if (selectedIndex == -1) 0 else selectedIndex
- val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
- val count = previews.size
+ 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
val unit = if (isReverse) -1 else 1
var currIndex: Int
for (i in 0 until count) {
@@ -593,34 +642,45 @@ class CodeWhispererPopupManager {
if (currIndex < 0) {
currIndex += count
}
- if (isValidRecommendation(previews[currIndex])) {
+ if (isValidRecommendation(detailContexts[currIndex], userInput, typeahead)) {
return currIndex
}
}
return -1
}
- private fun getValidCount(): Int =
- CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo().count { isValidRecommendation(it) }
-
- private fun getValidSelectedIndex(selectedIndex: Int): Int {
- var curr = 0
-
- val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
- previews.forEachIndexed { index, preview ->
+ 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 ->
if (index == selectedIndex) {
- return curr
+ return currIndexIgnoreInvalid
}
- if (isValidRecommendation(preview)) {
- curr++
+ if (isValidRecommendation(value, userInput, typeahead)) {
+ currIndexIgnoreInvalid++
}
}
return -1
}
- private fun isValidRecommendation(preview: PreviewContext): Boolean {
- if (preview.detail.isDiscarded) return false
- return preview.detail.recommendation.content().startsWith(preview.userInput + preview.typeahead)
+ 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
}
companion object {
@@ -638,17 +698,29 @@ class CodeWhispererPopupManager {
}
interface CodeWhispererPopupStateChangeListener {
- fun stateChanged(sessionContext: SessionContext) {}
- fun scrolled(sessionContext: SessionContext) {}
+ fun stateChanged(states: InvocationContext, sessionContext: SessionContext) {}
+ fun scrolled(states: InvocationContext, sessionContext: SessionContext) {}
fun recommendationAdded(states: InvocationContext, sessionContext: SessionContext) {}
+
+ fun stateChanged(sessionContext: SessionContextNew) {}
+ fun scrolled(sessionContext: SessionContextNew) {}
+ fun recommendationAdded(states: InvocationContextNew, sessionContext: SessionContextNew) {}
}
interface CodeWhispererUserActionListener {
- 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) {}
+ 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: SessionContextNew, diff: String) {}
+ fun enter(sessionContext: SessionContextNew, diff: String) {}
+ fun type(sessionContext: SessionContextNew, diff: String) {}
+ fun navigatePrevious(sessionContext: SessionContextNew) {}
+ fun navigateNext(sessionContext: SessionContextNew) {}
+ fun beforeAccept(sessionContext: SessionContextNew) {}
+ fun afterAccept(states: InvocationContextNew, previews: List, sessionContext: SessionContextNew, rangeMarker: RangeMarker) {}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt
new file mode 100644
index 0000000000..85304037f4
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManagerNew.kt
@@ -0,0 +1,630 @@
+// 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
+
+import com.intellij.codeInsight.lookup.LookupManager
+import com.intellij.idea.AppMode
+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_ESCAPE
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.VisualPosition
+import com.intellij.openapi.editor.actionSystem.EditorActionManager
+import com.intellij.openapi.editor.actionSystem.TypedAction
+import com.intellij.openapi.editor.colors.EditorColors
+import com.intellij.openapi.editor.colors.EditorColorsListener
+import com.intellij.openapi.editor.colors.EditorColorsManager
+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.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.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.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.editor.CodeWhispererEditorManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
+import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints
+import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.inlineLabelConstraints
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager.Companion.CODEWHISPERER_POPUP_STATE_CHANGED
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager.Companion.CODEWHISPERER_USER_ACTION_PERFORMED
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererEditorActionHandlerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupBackspaceHandlerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupEnterHandlerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupEscHandler
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupTypedHandlerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererAcceptButtonActionListenerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererActionListenerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererNextButtonActionListenerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererPrevButtonActionListenerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListenerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
+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
+import software.aws.toolkits.resources.message
+import java.awt.Point
+import java.awt.Rectangle
+import java.awt.event.ComponentAdapter
+import java.awt.event.ComponentEvent
+import java.awt.event.ComponentListener
+import javax.swing.JButton
+import javax.swing.JComponent
+import javax.swing.JLabel
+
+@Service
+class CodeWhispererPopupManagerNew {
+ val popupComponents = CodeWhispererPopupComponentsNew()
+
+ var shouldListenerCancelPopup: Boolean = true
+ private set
+
+ init {
+ // Listen for global scheme changes
+ ApplicationManager.getApplication().messageBus.connect().subscribe(
+ EditorColorsManager.TOPIC,
+ EditorColorsListener { scheme ->
+ if (scheme == null) return@EditorColorsListener
+ popupComponents.apply {
+ panel.background = scheme.defaultBackground
+ panel.components.forEach {
+ it.background = scheme.getColor(EditorColors.DOCUMENTATION_COLOR)
+ it.foreground = scheme.defaultForeground
+ }
+ buttonsPanel.components.forEach {
+ it.foreground = UIUtil.getLabelForeground()
+ }
+ recommendationInfoLabel.foreground = UIUtil.getLabelForeground()
+ codeReferencePanel.components.forEach {
+ it.background = scheme.getColor(EditorColors.DOCUMENTATION_COLOR)
+ it.foreground = UIUtil.getLabelForeground()
+ }
+ }
+ }
+ )
+ }
+
+ @RequiresEdt
+ fun changeStatesForNavigation(sessionContext: SessionContextNew, indexChange: Int) {
+ val validCount = getValidCount()
+ val validSelectedIndex = getValidSelectedIndex(sessionContext.selectedIndex)
+ if ((validSelectedIndex == validCount - 1 && indexChange == 1) ||
+ (validSelectedIndex == 0 && indexChange == -1)
+ ) {
+ return
+ }
+ val isReverse = indexChange < 0
+ val selectedIndex = findNewSelectedIndex(isReverse, sessionContext.selectedIndex + indexChange)
+
+ sessionContext.selectedIndex = selectedIndex
+
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
+ sessionContext
+ )
+ }
+
+ @RequiresEdt
+ fun changeStatesForTypeahead(
+ sessionContext: SessionContextNew,
+ typeaheadChange: String,
+ typeaheadAdded: Boolean,
+ ) {
+ if (!updateTypeahead(typeaheadChange, typeaheadAdded)) return
+ if (!updateSessionSelectedIndex(sessionContext)) return
+
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
+ sessionContext
+ )
+ }
+
+ @RequiresEdt
+ fun changeStatesForShowing(sessionContext: SessionContextNew, states: InvocationContextNew, recommendationAdded: Boolean = false) {
+ if (recommendationAdded) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED)
+ .recommendationAdded(states, sessionContext)
+ return
+ }
+
+ if (!updateSessionSelectedIndex(sessionContext)) return
+ if (sessionContext.popupOffset == -1) {
+ sessionContext.popupOffset = sessionContext.editor.caretModel.offset
+ }
+
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
+ sessionContext
+ )
+ }
+
+ private fun updateTypeahead(typeaheadChange: String, typeaheadAdded: Boolean): Boolean {
+ val recommendations = CodeWhispererServiceNew.getInstance().getAllPaginationSessions().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" }
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ return false
+ }
+ it.recommendationContext.typeahead.substring(
+ 0,
+ it.recommendationContext.typeahead.length - typeaheadChange.length
+ )
+ }
+ it.recommendationContext.typeahead = newTypeahead
+ }
+ return true
+ }
+
+ private fun updateSessionSelectedIndex(sessionContext: SessionContextNew): Boolean {
+ val selectedIndex = findNewSelectedIndex(false, sessionContext.selectedIndex)
+ if (selectedIndex == -1) {
+ LOG.debug { "None of the recommendation is valid at this point, cancelling the popup" }
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ return false
+ }
+
+ sessionContext.selectedIndex = selectedIndex
+ return true
+ }
+
+ fun updatePopupPanel(sessionContext: SessionContextNew?) {
+ if (sessionContext == null || sessionContext.selectedIndex == -1 || sessionContext.isDisposed()) return
+ val selectedIndex = sessionContext.selectedIndex
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ if (selectedIndex >= previews.size) return
+ val validCount = getValidCount()
+ val validSelectedIndex = getValidSelectedIndex(selectedIndex)
+ updateSelectedRecommendationLabelText(validSelectedIndex, validCount)
+ updateNavigationPanel(validSelectedIndex, validCount)
+ updateImportPanel(previews[selectedIndex].detail.recommendation.mostRelevantMissingImports())
+ updateCodeReferencePanel(sessionContext.project, previews[selectedIndex].detail.recommendation.references())
+ }
+
+ fun render(sessionContext: SessionContextNew, isRecommendationAdded: 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
+ // end time, and emit the event if it's at the pagination end.
+ // 2. New recommendations being added to the existing ones, we should not update the latency end time, and emit
+ // the event if it's at the pagination end.
+ // 3. User scrolling (so popup is changing positions), we should not update the latency end time and should not
+ // 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 (!CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) {
+ sessionContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime()
+ sessionContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime()
+ val triggerTypeOfLastTrigger = CodeWhispererServiceNew.getInstance().getAllPaginationSessions()
+ .values.filterNotNull().last().requestContext.triggerTypeInfo.triggerType
+ sessionContext.latencyContext.perceivedLatency =
+ sessionContext.latencyContext.getPerceivedLatency(triggerTypeOfLastTrigger)
+ }
+ if (isRecommendationAdded) return
+ showPopup(sessionContext)
+ }
+
+ fun dontClosePopupAndRun(runnable: () -> Unit) {
+ try {
+ shouldListenerCancelPopup = false
+ runnable()
+ } finally {
+ shouldListenerCancelPopup = true
+ }
+ }
+
+ fun showPopup(sessionContext: SessionContextNew, force: Boolean = false) {
+ val p = sessionContext.editor.offsetToXY(sessionContext.popupOffset)
+ val popup: JBPopup?
+ if (sessionContext.popup == null) {
+ popup = initPopup()
+ sessionContext.popup = popup
+ CodeWhispererInvocationStatusNew.getInstance().setPopupStartTimestamp()
+ initPopupListener(sessionContext, popup)
+ } else {
+ popup = sessionContext.popup
+ }
+ val editor = sessionContext.editor
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ val userInputOriginal = previews[sessionContext.selectedIndex].userInput
+ val userInputLines = userInputOriginal.split("\n").size - 1
+ val popupSize = (popup as AbstractPopup).preferredContentSize
+ val yAboveFirstLine = p.y - popupSize.height + userInputLines * editor.lineHeight
+ val popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height)
+ val editorRect = editor.scrollingModel.visibleArea
+ var shouldHidePopup = false
+
+ CodeWhispererInvocationStatusNew.getInstance().setDisplaySessionActive(true)
+
+ if (!editorRect.contains(popupRect)) {
+ // popup location above first line don't work, so don't show the popup
+ shouldHidePopup = true
+ }
+
+ // popup to always display above the current editing line
+ val popupLocation = Point(p.x, yAboveFirstLine)
+
+ val relativePopupLocationToEditor = RelativePoint(editor.contentComponent, popupLocation)
+
+ // TODO: visibleAreaChanged listener is not getting triggered in remote environment when scrolling
+ if (popup.isVisible) {
+ // Changing the position of BackendBeAbstractPopup does not work
+ if (!shouldHidePopup && !AppMode.isRemoteDevHost()) {
+ popup.setLocation(relativePopupLocationToEditor.screenPoint)
+ popup.size = popup.preferredContentSize
+ }
+ } else {
+ if (!AppMode.isRemoteDevHost()) {
+ 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
+ val caretVisualPosition = editor.offsetToVisualPosition(editor.caretModel.offset)
+
+ // display popup x lines below the caret where x is # of lines of suggestions, since inlays don't
+ // count as visual lines, the final math will always be just increment 1 line.
+ val popupPositionForRemote = VisualPosition(
+ caretVisualPosition.line + 1,
+ caretVisualPosition.column
+ )
+ editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, popupPositionForRemote)
+ popup.showInBestPositionFor(editor)
+ }
+ }
+
+ 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)
+ }
+ }
+
+ fun initPopup(): JBPopup = JBPopupFactory.getInstance()
+ .createComponentPopupBuilder(popupComponents.panel, null)
+ .setAlpha(0.1F)
+ .setCancelOnClickOutside(true)
+ .setCancelOnWindowDeactivation(true)
+ .createPopup()
+
+ fun getReformattedRecommendation(detailContext: DetailContextNew, userInput: String) =
+ detailContext.reformatted.content().substring(userInput.length)
+
+ private fun initPopupListener(sessionContext: SessionContextNew, popup: JBPopup) {
+ addPopupListener(popup)
+ sessionContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListenerNew(sessionContext), sessionContext)
+ addButtonActionListeners(sessionContext)
+ addMessageSubscribers(sessionContext)
+ setPopupActionHandlers(sessionContext)
+ addComponentListeners(sessionContext)
+ }
+
+ private fun addPopupListener(popup: JBPopup) {
+ val listener = CodeWhispererPopupListenerNew()
+ popup.addListener(listener)
+ Disposer.register(popup) {
+ popup.removeListener(listener)
+ }
+ }
+
+ private fun addMessageSubscribers(sessionContext: SessionContextNew) {
+ val connect = ApplicationManager.getApplication().messageBus.connect(sessionContext)
+ connect.subscribe(
+ CODEWHISPERER_USER_ACTION_PERFORMED,
+ object : CodeWhispererUserActionListener {
+ override fun navigateNext(sessionContext: SessionContextNew) {
+ changeStatesForNavigation(sessionContext, 1)
+ }
+
+ override fun navigatePrevious(sessionContext: SessionContextNew) {
+ changeStatesForNavigation(sessionContext, -1)
+ }
+
+ override fun backspace(sessionContext: SessionContextNew, diff: String) {
+ changeStatesForTypeahead(sessionContext, diff, false)
+ }
+
+ override fun enter(sessionContext: SessionContextNew, diff: String) {
+ changeStatesForTypeahead(sessionContext, diff, true)
+ }
+
+ override fun type(sessionContext: SessionContextNew, diff: String) {
+ // remove the character at primaryCaret if it's the same as the typed character
+ 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(sessionContext.project) {
+ document.deleteString(caretOffset, caretOffset + 1)
+ }
+ }
+ changeStatesForTypeahead(sessionContext, diff, true)
+ }
+
+ override fun beforeAccept(sessionContext: SessionContextNew) {
+ dontClosePopupAndRun {
+ CodeWhispererEditorManagerNew.getInstance().updateEditorWithRecommendation(sessionContext)
+ }
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(true)
+ }
+ }
+ )
+ }
+
+ private fun addButtonActionListeners(sessionContext: SessionContextNew) {
+ popupComponents.prevButton.addButtonActionListener(CodeWhispererPrevButtonActionListenerNew(sessionContext), sessionContext)
+ popupComponents.nextButton.addButtonActionListener(CodeWhispererNextButtonActionListenerNew(sessionContext), sessionContext)
+ popupComponents.acceptButton.addButtonActionListener(CodeWhispererAcceptButtonActionListenerNew(sessionContext), sessionContext)
+ }
+
+ private fun JButton.addButtonActionListener(listener: CodeWhispererActionListenerNew, sessionContext: SessionContextNew) {
+ this.addActionListener(listener)
+ Disposer.register(sessionContext) { this.removeActionListener(listener) }
+ }
+
+ private fun setPopupActionHandlers(sessionContext: SessionContextNew) {
+ val actionManager = EditorActionManager.getInstance()
+
+ sessionContext.project.putUserData(CodeWhispererServiceNew.KEY_SESSION_CONTEXT, sessionContext)
+
+ setPopupTypedHandler(CodeWhispererPopupTypedHandlerNew(TypedAction.getInstance().rawHandler, sessionContext), sessionContext)
+ setPopupActionHandler(ACTION_EDITOR_ESCAPE, CodeWhispererPopupEscHandler(sessionContext), sessionContext)
+ setPopupActionHandler(
+ ACTION_EDITOR_ENTER,
+ CodeWhispererPopupEnterHandlerNew(actionManager.getActionHandler(ACTION_EDITOR_ENTER), sessionContext),
+ sessionContext
+ )
+ setPopupActionHandler(
+ ACTION_EDITOR_BACKSPACE,
+ CodeWhispererPopupBackspaceHandlerNew(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), sessionContext),
+ sessionContext
+ )
+ }
+
+ private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandlerNew, sessionContext: SessionContextNew) {
+ val oldTypedHandler = TypedAction.getInstance().setupRawHandler(newHandler)
+ Disposer.register(sessionContext) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) }
+ }
+
+ private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandlerNew, sessionContext: SessionContextNew) {
+ val oldHandler = EditorActionManager.getInstance().setActionHandler(id, newHandler)
+ Disposer.register(sessionContext) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) }
+ }
+
+ private fun addComponentListeners(sessionContext: SessionContextNew) {
+ val editor = sessionContext.editor
+ val codeWhispererSelectionListener: SelectionListener = object : SelectionListener {
+ override fun selectionChanged(event: SelectionEvent) {
+ if (shouldListenerCancelPopup) {
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ }
+ super.selectionChanged(event)
+ }
+ }
+ editor.selectionModel.addSelectionListener(codeWhispererSelectionListener)
+ Disposer.register(sessionContext) { editor.selectionModel.removeSelectionListener(codeWhispererSelectionListener) }
+
+ val codeWhispererDocumentListener: DocumentListener = object : DocumentListener {
+ override fun documentChanged(event: DocumentEvent) {
+ if (shouldListenerCancelPopup) {
+ // 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 {
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ }
+ }
+ super.documentChanged(event)
+ }
+ }
+ editor.document.addDocumentListener(codeWhispererDocumentListener, sessionContext)
+
+ val codeWhispererCaretListener: CaretListener = object : CaretListener {
+ override fun caretPositionChanged(event: CaretEvent) {
+ if (shouldListenerCancelPopup) {
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ }
+ super.caretPositionChanged(event)
+ }
+ }
+ 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(e: ComponentEvent) {
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ super.componentMoved(e)
+ }
+
+ override fun componentShown(e: ComponentEvent?) {
+ CodeWhispererServiceNew.getInstance().disposeDisplaySession(false)
+ super.componentShown(e)
+ }
+ }
+ window?.addComponentListener(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 {
+ bringSuggestionInlayToFront(sessionContext.editor, sessionContext.popup, opposite = true)
+ }
+ super.mouseMoved(e)
+ }
+ }
+ editor.addEditorMouseMotionListener(suggestionHoverEnterListener, sessionContext)
+ }
+
+ private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) {
+ if (CodeWhispererInvocationStatusNew.getInstance().hasExistingServiceInvocation()) {
+ popupComponents.recommendationInfoLabel.text = message("codewhisperer.popup.pagination_info")
+ LOG.debug { "Pagination in progress. Current total: $validCount" }
+ } else {
+ popupComponents.recommendationInfoLabel.text =
+ message(
+ "codewhisperer.popup.recommendation_info",
+ validSelectedIndex + 1,
+ validCount,
+ POPUP_DIM_HEX
+ )
+ LOG.debug { "Updated popup recommendation label text. Index: $validSelectedIndex, total: $validCount" }
+ }
+ }
+
+ private fun updateNavigationPanel(validSelectedIndex: Int, validCount: Int) {
+ val multipleRecommendation = validCount > 1
+ popupComponents.prevButton.isEnabled = multipleRecommendation && validSelectedIndex != 0
+ popupComponents.nextButton.isEnabled = multipleRecommendation && validSelectedIndex != validCount - 1
+ }
+
+ private fun updateImportPanel(imports: List) {
+ popupComponents.panel.apply {
+ if (components.contains(popupComponents.importPanel)) {
+ remove(popupComponents.importPanel)
+ }
+ }
+ if (imports.isEmpty()) return
+
+ val firstImport = imports.first()
+ val choice = if (imports.size > 2) 2 else imports.size - 1
+ val message = message("codewhisperer.popup.import_info", firstImport.statement(), imports.size - 1, choice)
+ popupComponents.panel.add(popupComponents.importPanel, horizontalPanelConstraints)
+ popupComponents.importLabel.text = message
+ }
+
+ private fun updateCodeReferencePanel(project: Project, references: List) {
+ popupComponents.panel.apply {
+ if (components.contains(popupComponents.codeReferencePanel)) {
+ remove(popupComponents.codeReferencePanel)
+ }
+ }
+ if (references.isEmpty()) return
+
+ popupComponents.panel.add(popupComponents.codeReferencePanel, horizontalPanelConstraints)
+ val licenses = references.map { it.licenseName() }.toSet()
+ popupComponents.codeReferencePanelLink.apply {
+ actionListeners.toList().forEach {
+ removeActionListener(it)
+ }
+ addActionListener {
+ CodeWhispererCodeReferenceManager.getInstance(project).showCodeReferencePanel()
+ }
+ }
+ popupComponents.licenseCodePanel.apply {
+ removeAll()
+ add(popupComponents.licenseCodeLabelPrefixText, inlineLabelConstraints)
+ licenses.forEachIndexed { i, license ->
+ add(popupComponents.licenseLink(license), inlineLabelConstraints)
+ if (i == licenses.size - 1) return@forEachIndexed
+ add(JLabel(", "), inlineLabelConstraints)
+ }
+
+ add(JLabel(". "), inlineLabelConstraints)
+ add(popupComponents.codeReferencePanelLink, inlineLabelConstraints)
+ addHorizontalGlue()
+ }
+ popupComponents.licenseCodePanel.components.forEach {
+ if (it !is JComponent) return@forEach
+ it.font = it.font.deriveFont(POPUP_INFO_TEXT_SIZE)
+ }
+ }
+
+ fun findNewSelectedIndex(isReverse: Boolean, selectedIndex: Int): Int {
+ val start = if (selectedIndex == -1) 0 else selectedIndex
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ val count = previews.size
+ val unit = if (isReverse) -1 else 1
+ var currIndex: Int
+ for (i in 0 until count) {
+ currIndex = (start + i * unit) % count
+ if (currIndex < 0) {
+ currIndex += count
+ }
+ if (isValidRecommendation(previews[currIndex])) {
+ return currIndex
+ }
+ }
+ return -1
+ }
+
+ private fun getValidCount(): Int =
+ CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo().count { isValidRecommendation(it) }
+
+ private fun getValidSelectedIndex(selectedIndex: Int): Int {
+ var curr = 0
+
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ previews.forEachIndexed { index, preview ->
+ if (index == selectedIndex) {
+ return curr
+ }
+ if (isValidRecommendation(preview)) {
+ curr++
+ }
+ }
+ return -1
+ }
+
+ private fun isValidRecommendation(preview: PreviewContext): Boolean {
+ if (preview.detail.isDiscarded) return false
+ return preview.detail.recommendation.content().startsWith(preview.userInput + preview.typeahead)
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ fun getInstance(): CodeWhispererPopupManagerNew = service()
+ }
+}
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 967e691362..e4bb87feec 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
@@ -10,31 +10,27 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.xdebugger.ui.DebuggerColors
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayManager
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
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener {
- override fun stateChanged(sessionContext: SessionContext) {
- val editor = sessionContext.editor
+ override fun stateChanged(states: InvocationContext, sessionContext: SessionContext) {
+ val editor = states.requestContext.editor
val editorManager = CodeWhispererEditorManager.getInstance()
- val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
val selectedIndex = sessionContext.selectedIndex
- val typeahead = previews[selectedIndex].typeahead
- val detail = previews[selectedIndex].detail
+ val typeahead = sessionContext.typeahead
+ val detail = states.recommendationContext.details[selectedIndex]
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,
- previews[selectedIndex].userInput,
+ states.recommendationContext.userInputSinceInvocation
).substring(typeahead.length)
val remainingLines = remaining.split("\n")
@@ -65,7 +61,7 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener {
},
HighlighterTargetArea.EXACT_RANGE
)
- Disposer.register(sessionContext) {
+ Disposer.register(states.popup) {
editor.markupModel.removeHighlighter(rangeHighlighter)
}
sessionContext.toBeRemovedHighlighter = rangeHighlighter
@@ -91,18 +87,57 @@ 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))
- CodeWhispererInlayManagerNew.getInstance().updateInlays(sessionContext, inlayChunks)
+ CodeWhispererInlayManager.getInstance().updateInlays(states, inlayChunks)
CodeWhispererPopupManager.getInstance().render(
+ states,
sessionContext,
- isRecommendationAdded = false
+ overlappingLinesCount,
+ isRecommendationAdded = false,
+ isScrolling = false
)
}
- override fun scrolled(sessionContext: SessionContext) {
- CodeWhispererPopupManager.getInstance().render(sessionContext, isRecommendationAdded = 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 recommendationAdded(states: InvocationContext, sessionContext: SessionContext) {
- CodeWhispererPopupManager.getInstance().render(sessionContext, isRecommendationAdded = true)
+ CodeWhispererPopupManager.getInstance().render(
+ states,
+ sessionContext,
+ 0,
+ isRecommendationAdded = true,
+ isScrolling = false
+ )
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt
new file mode 100644
index 0000000000..ac7ccbe21a
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListenerNew.kt
@@ -0,0 +1,108 @@
+// 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
+
+import com.intellij.openapi.editor.markup.EffectType
+import com.intellij.openapi.editor.markup.HighlighterLayer
+import com.intellij.openapi.editor.markup.HighlighterTargetArea
+import com.intellij.openapi.editor.markup.TextAttributes
+import com.intellij.openapi.util.Disposer
+import com.intellij.xdebugger.ui.DebuggerColors
+import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
+
+class CodeWhispererUIChangeListenerNew : CodeWhispererPopupStateChangeListener {
+ override fun stateChanged(sessionContext: SessionContextNew) {
+ val editor = sessionContext.editor
+ val editorManager = CodeWhispererEditorManagerNew.getInstance()
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ val selectedIndex = sessionContext.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 = CodeWhispererPopupManagerNew.getInstance().getReformattedRecommendation(
+ detail,
+ previews[selectedIndex].userInput,
+ ).substring(typeahead.length)
+
+ val remainingLines = remaining.split("\n")
+ val firstLineOfRemaining = remainingLines.first()
+ val otherLinesOfRemaining = remainingLines.drop(1)
+
+ // process first line inlays, where we do subsequence matching as much as possible
+ val matchingSymbols = editorManager.getMatchingSymbolsFromRecommendation(
+ editor,
+ firstLineOfRemaining,
+ detail.isTruncatedOnRight,
+ sessionContext
+ )
+
+ sessionContext.toBeRemovedHighlighter?.let {
+ editor.markupModel.removeHighlighter(it)
+ }
+
+ // Add the strike-though hint for the remaining non-matching first-line right context for multi-line completions
+ if (!detail.isTruncatedOnRight && otherLinesOfRemaining.isNotEmpty()) {
+ val rangeHighlighter = editor.markupModel.addRangeHighlighter(
+ matchingSymbols[matchingSymbols.size - 2].second,
+ lineEndOffset,
+ HighlighterLayer.LAST + 1,
+ TextAttributes().apply {
+ effectType = EffectType.STRIKEOUT
+ effectColor = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE).foregroundColor
+ },
+ HighlighterTargetArea.EXACT_RANGE
+ )
+ Disposer.register(sessionContext) {
+ editor.markupModel.removeHighlighter(rangeHighlighter)
+ }
+ sessionContext.toBeRemovedHighlighter = rangeHighlighter
+ }
+
+ val chunks = CodeWhispererRecommendationManager.getInstance().buildRecommendationChunks(
+ firstLineOfRemaining,
+ matchingSymbols
+ )
+
+ // process other lines inlays, where we do tail-head matching as much as possible
+ val overlappingLinesCount = editorManager.findOverLappingLines(
+ editor,
+ otherLinesOfRemaining,
+ detail.isTruncatedOnRight,
+ sessionContext
+ )
+
+ var otherLinesInlayText = ""
+ otherLinesOfRemaining.subList(0, otherLinesOfRemaining.size - overlappingLinesCount).forEach {
+ otherLinesInlayText += "\n" + it
+ }
+
+ // 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))
+ CodeWhispererInlayManagerNew.getInstance().updateInlays(sessionContext, inlayChunks)
+ CodeWhispererPopupManagerNew.getInstance().render(
+ sessionContext,
+ isRecommendationAdded = false
+ )
+ }
+
+ override fun scrolled(sessionContext: SessionContextNew) {
+ CodeWhispererPopupManagerNew.getInstance().render(sessionContext, isRecommendationAdded = false)
+ }
+
+ override fun recommendationAdded(states: InvocationContextNew, sessionContext: SessionContextNew) {
+ CodeWhispererPopupManagerNew.getInstance().render(sessionContext, isRecommendationAdded = true)
+ }
+}
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 a2cea3c836..90962ac187 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,8 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
-abstract class CodeWhispererEditorActionHandler(val sessionContext: SessionContext) : EditorActionHandler()
+abstract class CodeWhispererEditorActionHandler(val states: InvocationContext) : EditorActionHandler()
+abstract class CodeWhispererEditorActionHandlerNew(val sessionContext: SessionContextNew) : 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 a3f40b8eec..eee58c0ddc 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,35 @@ 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.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew
class CodeWhispererPopupBackspaceHandler(
private val defaultHandler: EditorActionHandler,
- sessionContext: SessionContext,
-) : CodeWhispererEditorActionHandler(sessionContext) {
+ states: InvocationContext,
+) : CodeWhispererEditorActionHandler(states) {
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)
+ val newOffset = editor.caretModel.offset
+ val newText = "a".repeat(oldOffset - newOffset)
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
+ ).backspace(states, newText)
+ }
+ }
+}
+
+class CodeWhispererPopupBackspaceHandlerNew(
+ private val defaultHandler: EditorActionHandler,
+ sessionContext: SessionContextNew,
+) : CodeWhispererEditorActionHandlerNew(sessionContext) {
+ override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
+ val popupManager = CodeWhispererPopupManagerNew.getInstance()
popupManager.dontClosePopupAndRun {
val oldOffset = editor.caretModel.offset
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 1b165f76f1..f7f29c49f8 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,15 +9,35 @@ 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.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew
class CodeWhispererPopupEnterHandler(
private val defaultHandler: EditorActionHandler,
- sessionContext: SessionContext,
-) : CodeWhispererEditorActionHandler(sessionContext) {
+ states: InvocationContext,
+) : CodeWhispererEditorActionHandler(states) {
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)
+ val newOffset = editor.caretModel.offset
+ val newText = editor.document.getText(TextRange.create(oldOffset, newOffset))
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
+ ).enter(states, newText)
+ }
+ }
+}
+
+class CodeWhispererPopupEnterHandlerNew(
+ private val defaultHandler: EditorActionHandler,
+ sessionContext: SessionContextNew,
+) : CodeWhispererEditorActionHandlerNew(sessionContext) {
+ override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
+ val popupManager = CodeWhispererPopupManagerNew.getInstance()
popupManager.dontClosePopupAndRun {
val oldOffset = editor.caretModel.offset
defaultHandler.execute(editor, caret, dataContext)
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
index 634f76e885..b628ad0c60 100644
--- 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
@@ -6,11 +6,11 @@ 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
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
-class CodeWhispererPopupEscHandler(sessionContext: SessionContext) : CodeWhispererEditorActionHandler(sessionContext) {
+class CodeWhispererPopupEscHandler(sessionContext: SessionContextNew) : CodeWhispererEditorActionHandlerNew(sessionContext) {
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
- CodeWhispererService.getInstance().disposeDisplaySession(false)
+ CodeWhispererServiceNew.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
new file mode 100644
index 0000000000..020e143480
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupLeftArrowHandler.kt
@@ -0,0 +1,19 @@
+// 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
new file mode 100644
index 0000000000..9efba65a80
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupRightArrowHandler.kt
@@ -0,0 +1,19 @@
+// 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
new file mode 100644
index 0000000000..c92eae9106
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/handlers/CodeWhispererPopupTabHandler.kt
@@ -0,0 +1,19 @@
+// 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 e7bb42f480..81baa037d2 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,15 +7,31 @@ 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.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew
class CodeWhispererPopupTypedHandler(
private val defaultHandler: TypedActionHandler,
- val sessionContext: SessionContext,
+ val states: InvocationContext,
) : 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())
+ }
+ }
+}
+
+class CodeWhispererPopupTypedHandlerNew(
+ private val defaultHandler: TypedActionHandler,
+ val sessionContext: SessionContextNew,
+) : TypedActionHandler {
+ override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) {
+ CodeWhispererPopupManagerNew.getInstance().dontClosePopupAndRun {
defaultHandler.execute(editor, charTyped, dataContext)
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
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 6a7a03e132..fe5696adb0 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,11 +4,20 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners
import com.intellij.openapi.application.ApplicationManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent
-class CodeWhispererAcceptButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) {
+class CodeWhispererAcceptButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
+ override fun actionPerformed(e: ActionEvent?) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
+ ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext)
+ }
+}
+
+class CodeWhispererAcceptButtonActionListenerNew(sessionContext: SessionContextNew) : CodeWhispererActionListenerNew(sessionContext) {
override fun actionPerformed(e: ActionEvent?) {
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
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 23f0975e66..368a4fde79 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,9 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import java.awt.event.ActionListener
-abstract class CodeWhispererActionListener(val sessionContext: SessionContext) : ActionListener
+abstract class CodeWhispererActionListener(val states: InvocationContext) : ActionListener
+abstract class CodeWhispererActionListenerNew(val sessionContext: SessionContextNew) : 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 d11f219a33..2b9418aaa3 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,11 +4,20 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners
import com.intellij.openapi.application.ApplicationManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent
-class CodeWhispererNextButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) {
+class CodeWhispererNextButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
+ override fun actionPerformed(e: ActionEvent?) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
+ ).navigateNext(states)
+ }
+}
+
+class CodeWhispererNextButtonActionListenerNew(sessionContext: SessionContextNew) : CodeWhispererActionListenerNew(sessionContext) {
override fun actionPerformed(e: ActionEvent?) {
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
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 273a40a8e1..44ad7f1e3f 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,11 +4,20 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners
import com.intellij.openapi.application.ApplicationManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent
-class CodeWhispererPrevButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) {
+class CodeWhispererPrevButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
+ override fun actionPerformed(e: ActionEvent?) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
+ ).navigatePrevious(states)
+ }
+}
+
+class CodeWhispererPrevButtonActionListenerNew(sessionContext: SessionContextNew) : CodeWhispererActionListenerNew(sessionContext) {
override fun actionPerformed(e: ActionEvent?) {
ApplicationManager.getApplication().messageBus.syncPublisher(
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
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 4ebea98162..658f64909d 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,15 +6,31 @@ 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.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
-class CodeWhispererScrollListener(private val sessionContext: SessionContext) : VisibleAreaListener {
+class CodeWhispererScrollListener(private val states: InvocationContext) : VisibleAreaListener {
override fun visibleAreaChanged(e: VisibleAreaEvent) {
val oldRect = e.oldRectangle
val newRect = e.newRectangle
- if (CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive() &&
+ if (CodeWhispererInvocationStatus.getInstance().isPopupActive() &&
+ (oldRect.x != newRect.x || oldRect.y != newRect.y)
+ ) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(
+ CodeWhispererPopupManager.CODEWHISPERER_POPUP_STATE_CHANGED
+ ).scrolled(states, CodeWhispererPopupManager.getInstance().sessionContext)
+ }
+ }
+}
+
+class CodeWhispererScrollListenerNew(private val sessionContext: SessionContextNew) : VisibleAreaListener {
+ override fun visibleAreaChanged(e: VisibleAreaEvent) {
+ val oldRect = e.oldRectangle
+ val newRect = e.newRectangle
+ if (CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive() &&
(oldRect.x != newRect.x || oldRect.y != newRect.y)
) {
ApplicationManager.getApplication().messageBus.syncPublisher(
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
index c536f0c0ca..fe73ff91e4 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
@@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service
import com.intellij.openapi.editor.Editor
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.telemetry.CodewhispererTriggerType
@@ -19,7 +20,11 @@ interface CodeWhispererAutoTriggerHandler {
val triggerTypeInfo = TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, automatedTriggerType)
LOG.debug { "autotriggering CodeWhisperer with type ${automatedTriggerType.telemetryType}" }
- CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererServiceNew.getInstance().showRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
+ } else {
+ CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
+ }
}
companion object {
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 545d8a5b1e..10f6438e88 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
@@ -17,11 +17,13 @@ import kotlinx.coroutines.launch
import org.apache.commons.collections4.queue.CircularFifoQueue
import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.core.coroutines.applicationCoroutineScope
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew
import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType
import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState
import software.aws.toolkits.telemetry.CodewhispererTriggerType
@@ -77,7 +79,14 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa
// real auto trigger logic
fun invoke(editor: Editor, triggerType: CodeWhispererAutomatedTriggerType): Job? {
- if (!CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger)) {
+ if (!(
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererServiceNew.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger)
+ } else {
+ CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger)
+ }
+ )
+ ) {
return null
}
@@ -170,7 +179,12 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa
var previousOneAccept: Double = 0.0
var previousOneReject: Double = 0.0
var previousOneOther: Double = 0.0
- val previousOneDecision = CodeWhispererTelemetryService.getInstance().previousUserTriggerDecision
+ val previousOneDecision =
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererTelemetryServiceNew.getInstance().previousUserTriggerDecision
+ } else {
+ CodeWhispererTelemetryService.getInstance().previousUserTriggerDecision
+ }
if (previousOneDecision == null) {
previousOneAccept = 0.0
previousOneReject = 0.0
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 ad078da6a0..9019195081 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt
@@ -9,14 +9,16 @@ import com.intellij.openapi.components.service
import com.intellij.util.messages.Topic
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
@Service
class CodeWhispererInvocationStatus {
- private val isInvokingService: AtomicBoolean = AtomicBoolean(false)
+ private val isInvokingCodeWhisperer: 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
@@ -24,22 +26,30 @@ class CodeWhispererInvocationStatus {
var popupStartTimestamp: Instant? = null
private set
- fun startInvocation() {
- isInvokingService.set(true)
- ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(true)
- LOG.debug { "Starting CodeWhisperer invocation" }
- }
+ 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 hasExistingServiceInvocation(): Boolean = isInvokingService.get()
+ fun hasExistingServiceInvocation(): Boolean = isInvokingCodeWhisperer.get()
fun finishInvocation() {
- if (isInvokingService.compareAndSet(true, false)) {
+ if (isInvokingCodeWhisperer.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()
}
@@ -55,13 +65,13 @@ class CodeWhispererInvocationStatus {
}
fun hasEnoughDelayToShowCodeWhisperer(): Boolean {
- val timeCanShowCodeWhisperer = timeAtLastDocumentChanged.plusMillis(50)
+ val timeCanShowCodeWhisperer = timeAtLastDocumentChanged.plusMillis(CodeWhispererConstants.POPUP_DELAY)
return timeCanShowCodeWhisperer.isBefore(Instant.now())
}
- fun isDisplaySessionActive(): Boolean = isPopupActive
+ fun isPopupActive(): Boolean = isPopupActive
- fun setDisplaySessionActive(value: Boolean) {
+ fun setPopupActive(value: Boolean) {
isPopupActive = value
}
@@ -74,6 +84,11 @@ 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/CodeWhispererInvocationStatusNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatusNew.kt
new file mode 100644
index 0000000000..2c465f8be9
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatusNew.kt
@@ -0,0 +1,81 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.service
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus.Companion.CODEWHISPERER_INVOCATION_STATE_CHANGED
+import java.time.Duration
+import java.time.Instant
+import java.util.concurrent.atomic.AtomicBoolean
+
+@Service
+class CodeWhispererInvocationStatusNew {
+ private val isInvokingService: AtomicBoolean = AtomicBoolean(false)
+ private var invokingSessionId: String? = null
+ var timeAtLastDocumentChanged: Instant = Instant.now()
+ private set
+ private var isPopupActive: Boolean = false
+ private var timeAtLastInvocationStart: Instant? = null
+ var popupStartTimestamp: Instant? = null
+ private set
+
+ fun startInvocation() {
+ isInvokingService.set(true)
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(true)
+ LOG.debug { "Starting CodeWhisperer invocation" }
+ }
+
+ fun hasExistingServiceInvocation(): Boolean = isInvokingService.get()
+
+ fun finishInvocation() {
+ if (isInvokingService.compareAndSet(true, false)) {
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(false)
+ LOG.debug { "Ending CodeWhisperer invocation" }
+ invokingSessionId = null
+ }
+ }
+
+ fun documentChanged() {
+ timeAtLastDocumentChanged = Instant.now()
+ }
+
+ fun setPopupStartTimestamp() {
+ popupStartTimestamp = Instant.now()
+ }
+
+ fun getTimeSinceDocumentChanged(): Double {
+ val timeSinceDocumentChanged = Duration.between(timeAtLastDocumentChanged, Instant.now())
+ val timeInDouble = timeSinceDocumentChanged.toMillis().toDouble()
+ return timeInDouble
+ }
+
+ fun hasEnoughDelayToShowCodeWhisperer(): Boolean {
+ val timeCanShowCodeWhisperer = timeAtLastDocumentChanged.plusMillis(50)
+ return timeCanShowCodeWhisperer.isBefore(Instant.now())
+ }
+
+ fun isDisplaySessionActive(): Boolean = isPopupActive
+
+ fun setDisplaySessionActive(value: Boolean) {
+ isPopupActive = value
+ }
+
+ fun setInvocationStart() {
+ timeAtLastInvocationStart = Instant.now()
+ }
+
+ fun setInvocationSessionId(sessionId: String?) {
+ LOG.debug { "Set current CodeWhisperer invocation sessionId: $sessionId" }
+ invokingSessionId = sessionId
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ fun getInstance(): CodeWhispererInvocationStatusNew = 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 ae85ce662b..486f2e2eba 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,8 +9,8 @@ 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.DetailContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.RequestContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType
import kotlin.math.max
import kotlin.math.min
@@ -49,6 +49,38 @@ class CodeWhispererRecommendationManager {
.build()
}
+ fun reformatReference(requestContext: RequestContextNew, recommendation: Completion): Completion {
+ // startOffset is the offset at the start of user input since invocation
+ val invocationStartOffset = requestContext.caretPosition.offset
+
+ val startOffsetSinceUserInput = requestContext.editor.caretModel.offset
+ val endOffset = invocationStartOffset + recommendation.content().length
+
+ if (startOffsetSinceUserInput > endOffset) return recommendation
+
+ val reformattedReferences = recommendation.references().filter {
+ val referenceStart = invocationStartOffset + it.recommendationContentSpan().start()
+ val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end()
+ referenceStart < endOffset && referenceEnd > startOffsetSinceUserInput
+ }.map {
+ val referenceStart = invocationStartOffset + it.recommendationContentSpan().start()
+ val referenceEnd = invocationStartOffset + it.recommendationContentSpan().end()
+ val updatedReferenceStart = max(referenceStart, startOffsetSinceUserInput)
+ val updatedReferenceEnd = min(referenceEnd, endOffset)
+ it.toBuilder().recommendationContentSpan(
+ Span.builder()
+ .start(updatedReferenceStart - invocationStartOffset)
+ .end(updatedReferenceEnd - invocationStartOffset)
+ .build()
+ ).build()
+ }
+
+ return Completion.builder()
+ .content(recommendation.content())
+ .references(reformattedReferences)
+ .build()
+ }
+
fun buildRecommendationChunks(
recommendation: String,
matchingSymbols: List>,
@@ -64,7 +96,7 @@ class CodeWhispererRecommendationManager {
userInput: String,
recommendations: List,
requestId: String,
- ): MutableList {
+ ): List {
val seen = mutableSetOf()
return recommendations.map {
val isDiscardedByUserInput = !it.content().startsWith(userInput) || it.content() == userInput
@@ -130,6 +162,77 @@ class CodeWhispererRecommendationManager {
}.toMutableList()
}
+ fun buildDetailContext(
+ requestContext: RequestContextNew,
+ userInput: String,
+ recommendations: List,
+ requestId: String,
+ ): MutableList {
+ val seen = mutableSetOf()
+ return recommendations.map {
+ val isDiscardedByUserInput = !it.content().startsWith(userInput) || it.content() == userInput
+ if (isDiscardedByUserInput) {
+ return@map DetailContextNew(
+ requestId,
+ it,
+ it,
+ isDiscarded = true,
+ isTruncatedOnRight = false,
+ rightOverlap = "",
+ getCompletionType(it)
+ )
+ }
+
+ val overlap = findRightContextOverlap(requestContext, it)
+ val overlapIndex = it.content().lastIndexOf(overlap)
+ val truncatedContent =
+ if (overlap.isNotEmpty() && overlapIndex >= 0) {
+ it.content().substring(0, overlapIndex)
+ } else {
+ it.content()
+ }
+ val truncated = it.toBuilder()
+ .content(truncatedContent)
+ .build()
+ val isDiscardedByUserInputForTruncated = !truncated.content().startsWith(userInput) || truncated.content() == userInput
+ if (isDiscardedByUserInputForTruncated) {
+ return@map DetailContextNew(
+ requestId,
+ it,
+ truncated,
+ isDiscarded = true,
+ isTruncatedOnRight = true,
+ rightOverlap = overlap,
+ getCompletionType(it)
+ )
+ }
+
+ val isDiscardedByRightContextTruncationDedupe = !seen.add(truncated.content())
+ val isDiscardedByBlankAfterTruncation = truncated.content().isBlank()
+ if (isDiscardedByRightContextTruncationDedupe || isDiscardedByBlankAfterTruncation) {
+ return@map DetailContextNew(
+ requestId,
+ it,
+ truncated,
+ isDiscarded = true,
+ truncated.content().length != it.content().length,
+ overlap,
+ getCompletionType(it)
+ )
+ }
+ val reformatted = reformatReference(requestContext, truncated)
+ DetailContextNew(
+ requestId,
+ it,
+ reformatted,
+ isDiscarded = false,
+ truncated.content().length != it.content().length,
+ overlap,
+ getCompletionType(it)
+ )
+ }.toMutableList()
+ }
+
fun findRightContextOverlap(
requestContext: RequestContext,
recommendation: Completion,
@@ -141,6 +244,17 @@ class CodeWhispererRecommendationManager {
return findRightContextOverlap(rightContext, recommendationContent)
}
+ fun findRightContextOverlap(
+ requestContext: RequestContextNew,
+ recommendation: Completion,
+ ): String {
+ val document = requestContext.editor.document
+ val caret = requestContext.editor.caretModel.primaryCaret
+ val rightContext = document.charsSequence.subSequence(caret.offset, document.charsSequence.length).toString()
+ val recommendationContent = recommendation.content()
+ return findRightContextOverlap(rightContext, recommendationContent)
+ }
+
@VisibleForTesting
fun findRightContextOverlap(rightContext: String, recommendationContent: String): String {
val rightContextFirstLine = rightContext.substringBefore("\n")
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 49ef7e1edc..f11c501402 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
@@ -15,14 +15,14 @@ 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 kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -46,8 +46,11 @@ import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.coroutines.EDT
+import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT
@@ -59,14 +62,12 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhisper
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJson
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition
import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.RequestContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
@@ -95,10 +96,6 @@ 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
init {
Disposer.register(this, codeInsightSettingsFacade)
@@ -166,22 +163,13 @@ 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)
+ getRequestContext(triggerTypeInfo, editor, project, psiFile, latencyContext)
} 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
@@ -203,7 +191,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
}
LOG.debug {
- "Calling CodeWhisperer service, jobId: $currentJobId, trigger type: ${triggerTypeInfo.triggerType}" +
+ "Calling CodeWhisperer service, trigger type: ${triggerTypeInfo.triggerType}" +
if (triggerTypeInfo.triggerType == CodewhispererTriggerType.AutoTrigger) {
", auto-trigger type: ${triggerTypeInfo.automatedTriggerType}"
} else {
@@ -211,163 +199,208 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
}
}
- CodeWhispererInvocationStatus.getInstance().startInvocation()
+ val invocationStatus = CodeWhispererInvocationStatus.getInstance()
+ if (invocationStatus.checkExistingInvocationAndSet()) {
+ return
+ }
- invokeCodeWhispererInBackground(requestContext, currentJobId, latencyContext)
+ invokeCodeWhispererInBackground(requestContext)
}
- internal suspend fun invokeCodeWhispererInBackground(requestContext: RequestContext, currentJobId: Int, latencyContext: LatencyContext) {
- 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
+ internal suspend fun invokeCodeWhispererInBackground(requestContext: RequestContext): Job {
+ val popup = withContext(EDT) {
+ CodeWhispererPopupManager.getInstance().initPopup().also {
+ Disposer.register(it) { CodeWhispererInvocationStatus.getInstance().finishInvocation() }
+ }
}
- this.sessionContext = sessionContext
val workerContexts = mutableListOf()
-
- // When session is disposed we will cancel this coroutine. The only places session can get disposed should be
- // from CodeWhispererService.disposeDisplaySession().
+ // When popup is disposed we will cancel this coroutine. The only places popup can get disposed should be
+ // from CodeWhispererPopupManager.cancelPopup() and CodeWhispererPopupManager.closePopup().
// It's possible and ok that coroutine will keep running until the next time we check it's state.
// As long as we don't show to the user extra info we are good.
+ val coroutineScope = disposableCoroutineScope(popup)
+
+ var states: InvocationContext? = null
var lastRecommendationIndex = -1
- try {
- val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator(
- buildCodeWhispererRequest(
- requestContext.fileContextInfo,
- requestContext.awaitSupplementalContext(),
- requestContext.customizationArn
+ val job = coroutineScope.launch {
+ try {
+ val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator(
+ buildCodeWhispererRequest(
+ requestContext.fileContextInfo,
+ requestContext.awaitSupplementalContext(),
+ requestContext.customizationArn
+ )
)
- )
- var startTime = System.nanoTime()
- latencyContext.codewhispererPreprocessingEnd = System.nanoTime()
- latencyContext.paginationAllCompletionsStart = System.nanoTime()
- CodeWhispererInvocationStatus.getInstance().setInvocationStart()
- var requestCount = 0
- for (response in responseIterable) {
- requestCount++
- val endTime = System.nanoTime()
- val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble()
- startTime = endTime
- val requestId = response.responseMetadata().requestId()
- val sessionId = response.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
- if (requestCount == 1) {
- latencyContext.codewhispererPostprocessingStart = System.nanoTime()
- latencyContext.paginationFirstCompletionTime = latency
- latencyContext.firstRequestId = requestId
- CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId)
- }
- if (response.nextToken().isEmpty()) {
- latencyContext.paginationAllCompletionsEnd = System.nanoTime()
- }
- val responseContext = ResponseContext(sessionId)
- logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null)
- lastRecommendationIndex += response.completions().size
- ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED)
- .onSuccess(requestContext.fileContextInfo)
- CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
- currentJobId,
- requestId,
- requestContext,
- responseContext,
- lastRecommendationIndex,
- true,
- latency,
- null
- )
+ var startTime = System.nanoTime()
+ requestContext.latencyContext.codewhispererPreprocessingEnd = System.nanoTime()
+ requestContext.latencyContext.paginationAllCompletionsStart = System.nanoTime()
+ CodeWhispererInvocationStatus.getInstance().setInvocationStart()
+ var requestCount = 0
+ for (response in responseIterable) {
+ requestCount++
+ val endTime = System.nanoTime()
+ val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble()
+ startTime = endTime
+ val requestId = response.responseMetadata().requestId()
+ val sessionId = response.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ if (requestCount == 1) {
+ requestContext.latencyContext.codewhispererPostprocessingStart = System.nanoTime()
+ requestContext.latencyContext.paginationFirstCompletionTime =
+ (endTime - requestContext.latencyContext.codewhispererEndToEndStart).toDouble()
+ requestContext.latencyContext.firstRequestId = requestId
+ CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId)
+ }
+ if (response.nextToken().isEmpty()) {
+ requestContext.latencyContext.paginationAllCompletionsEnd = System.nanoTime()
+ }
+ val responseContext = ResponseContext(sessionId)
+ logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null)
+ lastRecommendationIndex += response.completions().size
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED)
+ .onSuccess(requestContext.fileContextInfo)
+ CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
+ requestId,
+ requestContext,
+ responseContext,
+ lastRecommendationIndex,
+ true,
+ latency,
+ null
+ )
- val validatedResponse = validateResponse(response)
+ val validatedResponse = validateResponse(response)
- runInEdt {
- // If delay is not met, add them to the worker queue and process them later.
- // On first response, workers queue must be empty. If there's enough delay before showing,
- // process CodeWhisperer UI rendering and workers queue will remain empty throughout this
- // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task
- // will be added to the workers queue.
- // On subsequent responses, if they see workers queue is not empty, it means the first worker
- // task hasn't been finished yet, in this case simply add another task to the queue. If they
- // see worker queue is empty, the previous tasks must have been finished before this. In this
- // case render CodeWhisperer UI directly.
- val workerContext = WorkerContext(requestContext, responseContext, validatedResponse)
- if (workerContexts.isNotEmpty()) {
- workerContexts.add(workerContext)
- } else {
- if (ongoingRequests.values.filterNotNull().isEmpty() &&
- !CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer()
- ) {
- // It's the first response, and no enough delay before showing
- projectCoroutineScope(requestContext.project).launch {
- while (!CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer()) {
- delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL)
- }
- runInEdt {
- workerContexts.forEach {
- processCodeWhispererUI(
- sessionContext,
- it,
- ongoingRequests[currentJobId],
- cs,
- currentJobId
- )
- if (!ongoingRequests.contains(currentJobId)) {
- job?.cancel()
+ runInEdt {
+ // If delay is not met, add them to the worker queue and process them later.
+ // On first response, workers queue must be empty. If there's enough delay before showing,
+ // process CodeWhisperer UI rendering and workers queue will remain empty throughout this
+ // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task
+ // will be added to the workers queue.
+ // On subsequent responses, if they see workers queue is not empty, it means the first worker
+ // task hasn't been finished yet, in this case simply add another task to the queue. If they
+ // see worker queue is empty, the previous tasks must have been finished before this. In this
+ // case render CodeWhisperer UI directly.
+ val workerContext = WorkerContext(requestContext, responseContext, validatedResponse, popup)
+ if (workerContexts.isNotEmpty()) {
+ workerContexts.add(workerContext)
+ } else {
+ if (states == null && !popup.isDisposed &&
+ !CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer()
+ ) {
+ // It's the first response, and no enough delay before showing
+ projectCoroutineScope(requestContext.project).launch {
+ while (!CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToShowCodeWhisperer()) {
+ delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL)
+ }
+ runInEdt {
+ workerContexts.forEach {
+ states = processCodeWhispererUI(it, states)
}
+ workerContexts.clear()
}
- workerContexts.clear()
}
- }
- workerContexts.add(workerContext)
- } else {
- // Have enough delay before showing for the first response, or it's subsequent responses
- processCodeWhispererUI(
- sessionContext,
- workerContext,
- ongoingRequests[currentJobId],
- cs,
- currentJobId
- )
- if (!ongoingRequests.contains(currentJobId)) {
- job?.cancel()
+ workerContexts.add(workerContext)
+ } else {
+ // Have enough delay before showing for the first response, or it's subsequent responses
+ states = processCodeWhispererUI(workerContext, states)
}
}
}
+ if (!isActive) {
+ // If job is cancelled before we do another request, don't bother making
+ // another API call to save resources
+ LOG.debug { "Skipping sending remaining requests on CodeWhisperer session exit" }
+ break
+ }
}
- if (!cs.isActive) {
- // If job is cancelled before we do another request, don't bother making
- // another API call to save resources
- LOG.debug { "Skipping sending remaining requests on inactive CodeWhisperer session exit" }
- return
- }
- if (requestCount >= PAGINATION_REQUEST_COUNT_ALLOWED) {
- LOG.debug { "Only $PAGINATION_REQUEST_COUNT_ALLOWED request per pagination session for now" }
+ } catch (e: Exception) {
+ val requestId: String
+ val sessionId: String
+ val displayMessage: String
+
+ if (
+ CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) ||
+ e is ResourceNotFoundException
+ ) {
+ (e as CodeWhispererRuntimeException)
+
+ requestId = e.requestId() ?: ""
+ sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ val exceptionType = e::class.simpleName
+ val responseContext = ResponseContext(sessionId)
+
+ CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
+ requestId,
+ requestContext,
+ responseContext,
+ lastRecommendationIndex,
+ false,
+ 0.0,
+ exceptionType
+ )
+
+ LOG.debug {
+ "The provided customization ${requestContext.customizationArn} is not found, " +
+ "will fallback to the default and retry generate completion"
+ }
+ logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
+
+ notifyWarn(
+ title = "",
+ content = message("codewhisperer.notification.custom.not_available"),
+ project = requestContext.project,
+ notificationActions = listOf(
+ NotificationAction.create(
+ message("codewhisperer.notification.custom.simple.button.select_another_customization")
+ ) { _, notification ->
+ CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project)
+ notification.expire()
+ }
+ )
+ )
CodeWhispererInvocationStatus.getInstance().finishInvocation()
- break
+ CodeWhispererInvocationStatus.getInstance().setInvocationComplete()
+
+ requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) }
+
+ projectCoroutineScope(requestContext.project).launch {
+ showRecommendationsInPopup(
+ requestContext.editor,
+ requestContext.triggerTypeInfo,
+ requestContext.latencyContext
+ )
+ }
+ return@launch
+ } else if (e is CodeWhispererException) {
+ 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) {
+ 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 {
+ requestId = ""
+ sessionId = ""
+ val statusCode = if (e is SdkServiceException) e.statusCode() else 0
+ displayMessage =
+ if (statusCode >= 500) {
+ message("codewhisperer.trigger.error.server_side")
+ } else {
+ message("codewhisperer.trigger.error.client_side")
+ }
+ if (statusCode < 500) {
+ LOG.debug(e) { "Error invoking CodeWhisperer service" }
+ }
}
- }
- } catch (e: Exception) {
- val requestId: String
- val sessionId: String
- val displayMessage: String
-
- if (
- CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) ||
- e is ResourceNotFoundException
- ) {
- (e as CodeWhispererRuntimeException)
-
- requestId = e.requestId().orEmpty()
- sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
val exceptionType = e::class.simpleName
val responseContext = ResponseContext(sessionId)
-
+ CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId)
+ logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
- currentJobId,
requestId,
requestContext,
responseContext,
@@ -377,158 +410,95 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
exceptionType
)
- LOG.debug {
- "The provided customization ${requestContext.customizationArn} is not found, " +
- "will fallback to the default and retry generate completion"
- }
- logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
-
- notifyWarn(
- title = "",
- content = message("codewhisperer.notification.custom.not_available"),
- project = requestContext.project,
- notificationActions = listOf(
- NotificationAction.create(
- message("codewhisperer.notification.custom.simple.button.select_another_customization")
- ) { _, notification ->
- CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project)
- notification.expire()
- }
- )
- )
- CodeWhispererInvocationStatus.getInstance().finishInvocation()
-
- requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) }
-
- showRecommendationsInPopup(
- requestContext.editor,
- requestContext.triggerTypeInfo,
- latencyContext
- )
- return
- } else if (e is CodeWhispererException) {
- requestId = e.requestId().orEmpty()
- sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
- displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side")
- } else if (e is CodeWhispererRuntimeException) {
- requestId = e.requestId().orEmpty()
- sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
- displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side")
- } else {
- requestId = ""
- sessionId = ""
- val statusCode = if (e is SdkServiceException) e.statusCode() else 0
- displayMessage =
- if (statusCode >= 500) {
- message("codewhisperer.trigger.error.server_side")
- } else {
- message("codewhisperer.trigger.error.client_side")
+ if (e is ThrottlingException &&
+ e.message == CodeWhispererConstants.THROTTLING_MESSAGE
+ ) {
+ CodeWhispererExplorerActionManager.getInstance().setSuspended(requestContext.project)
+ if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ notifyErrorCodeWhispererUsageLimit(requestContext.project)
}
- if (statusCode < 500) {
- LOG.debug(e) { "Error invoking CodeWhisperer service" }
- }
- }
- val exceptionType = e::class.simpleName
- val responseContext = ResponseContext(sessionId)
- CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId)
- logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
- CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
- currentJobId,
- requestId,
- requestContext,
- responseContext,
- lastRecommendationIndex,
- false,
- 0.0,
- exceptionType
- )
-
- if (e is ThrottlingException &&
- e.message == CodeWhispererConstants.THROTTLING_MESSAGE
- ) {
- CodeWhispererExplorerActionManager.getInstance().setSuspended(requestContext.project)
- if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
- notifyErrorCodeWhispererUsageLimit(requestContext.project)
- }
- } else {
- if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
- // 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().isDisplaySessionActive()) {
+ } else {
+ if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ // We should only show error hint when CodeWhisperer popup is not visible,
+ // and make it silent if CodeWhisperer popup is showing.
+ if (!CodeWhispererInvocationStatus.getInstance().isPopupActive()) {
showCodeWhispererErrorHint(requestContext.editor, displayMessage)
}
}
}
- }
- CodeWhispererInvocationStatus.getInstance().finishInvocation()
- runInEdt {
- CodeWhispererPopupManager.getInstance().updatePopupPanel(sessionContext)
+ CodeWhispererInvocationStatus.getInstance().finishInvocation()
+ runInEdt {
+ states?.let {
+ CodeWhispererPopupManager.getInstance().updatePopupPanel(
+ it,
+ CodeWhispererPopupManager.getInstance().sessionContext
+ )
+ }
+ }
+ } finally {
+ CodeWhispererInvocationStatus.getInstance().setInvocationComplete()
}
}
+
+ return job
}
@RequiresEdt
- private fun processCodeWhispererUI(
- sessionContext: SessionContext,
- workerContext: WorkerContext,
- currStates: InvocationContext?,
- coroutine: CoroutineScope,
- jobId: Int,
- ) {
+ private fun processCodeWhispererUI(workerContext: WorkerContext, currStates: InvocationContext?): InvocationContext? {
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 (!coroutine.isActive || sessionContext.isDisposed()) {
- LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. RequestId: $requestId, jobId: $jobId" }
- return
+ if (popup.isDisposed) {
+ LOG.debug { "Stop showing CodeWhisperer recommendations on CodeWhisperer session exit. RequestId: $requestId" }
+ return null
}
if (requestContext.editor.isDisposed) {
- LOG.debug { "Stop showing all CodeWhisperer recommendations since editor is disposed. RequestId: $requestId, jobId: $jobId" }
- disposeDisplaySession(false)
- return
+ LOG.debug { "Stop showing CodeWhisperer recommendations since editor is disposed. RequestId: $requestId" }
+ CodeWhispererPopupManager.getInstance().cancelPopup(popup)
+ return null
}
- CodeWhispererInvocationStatus.getInstance().finishInvocation()
+ if (response.nextToken().isEmpty()) {
+ CodeWhispererInvocationStatus.getInstance().finishInvocation()
+ }
val caretMovement = CodeWhispererEditorManager.getInstance().getCaretMovement(
requestContext.editor,
requestContext.caretPosition
)
- val isPopupShowing = checkRecommendationsValidity(currStates, false)
+ val isPopupShowing: Boolean
val nextStates: InvocationContext?
if (currStates == null) {
- // first response for the jobId
- nextStates = initStates(jobId, requestContext, responseContext, response, caretMovement)
+ // first response
+ nextStates = initStates(requestContext, responseContext, response, caretMovement, popup)
+ isPopupShowing = false
- // receiving a null state means caret has moved backward,
- // so we are going to cancel the current job
+ // receiving a null state means caret has moved backward or there's a conflict with
+ // Intellisense popup, so we are going to cancel the job
if (nextStates == null) {
- return
+ LOG.debug { "Cancelling popup and exiting CodeWhisperer session. RequestId: $requestId" }
+ CodeWhispererPopupManager.getInstance().cancelPopup(popup)
+ return null
}
} else {
- // subsequent responses for the jobId
+ // subsequent responses
nextStates = updateStates(currStates, response)
+ isPopupShowing = checkRecommendationsValidity(currStates, false)
}
- LOG.debug { "Adding ${response.completions().size} completions to the session. RequestId: $requestId, jobId: $jobId" }
- // 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.count { !it.isDiscarded }
- LOG.debug { "Suggestions status: valid: $valid, discarded: ${allSuggestions.size - valid}" }
+ val hasAtLeastOneValid = checkRecommendationsValidity(nextStates, response.nextToken().isEmpty())
// 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
- // TODO: may have bug; visit later
- if (nextStates.recommendationContext.details.isEmpty()) {
+ if (nextStates.recommendationContext.details.isEmpty() && response.nextToken().isEmpty()) {
LOG.debug { "Received just an empty list from this session, requestId: $requestId" }
CodeWhispererTelemetryService.getInstance().sendUserDecisionEvent(
requestContext,
@@ -548,41 +518,38 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
)
}
if (!hasAtLeastOneValid) {
- 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)
+ if (response.nextToken().isEmpty()) {
+ LOG.debug { "None of the recommendations are valid, exiting CodeWhisperer session" }
+ CodeWhispererPopupManager.getInstance().cancelPopup(popup)
+ return null
}
} else {
- updateCodeWhisperer(sessionContext, nextStates, isPopupShowing)
+ updateCodeWhisperer(nextStates, isPopupShowing)
}
+ return nextStates
}
private fun initStates(
- jobId: Int,
requestContext: RequestContext,
responseContext: ResponseContext,
response: GenerateCompletionsResponse,
caretMovement: CaretMovement,
+ popup: JBPopup,
): InvocationContext? {
val requestId = response.responseMetadata().requestId()
val recommendations = response.completions()
val visualPosition = requestContext.editor.caretModel.visualPosition
+ if (CodeWhispererPopupManager.getInstance().hasConflictingPopups(requestContext.editor)) {
+ LOG.debug { "Detect conflicting popup window with CodeWhisperer popup, not showing CodeWhisperer popup" }
+ sendDiscardedUserDecisionEventForAll(requestContext, responseContext, recommendations)
+ return null
+ }
if (caretMovement == CaretMovement.MOVE_BACKWARD) {
- LOG.debug { "Caret moved backward, discarding all of the recommendations 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)
- disposeDisplaySession(false)
+ LOG.debug { "Caret moved backward, discarding all of the recommendations. Request ID: $requestId" }
+ sendDiscardedUserDecisionEventForAll(requestContext, responseContext, recommendations)
return null
}
-
val userInputOriginal = CodeWhispererEditorManager.getInstance().getUserInputSinceInvocation(
requestContext.editor,
requestContext.caretPosition.offset
@@ -606,9 +573,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
recommendations,
requestId
)
- val recommendationContext = RecommendationContext(detailContexts, userInputOriginal, userInput, visualPosition, jobId)
- ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext)
- return ongoingRequests[jobId]
+ val recommendationContext = RecommendationContext(detailContexts, userInputOriginal, userInput, visualPosition)
+ return buildInvocationContext(requestContext, responseContext, recommendationContext, popup)
}
private fun updateStates(
@@ -616,19 +582,24 @@ 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)
- recommendationContext.details.addAll(newDetailContexts)
- return states
+ val updatedStates = states.copy(
+ recommendationContext = recommendationContext.copy(details = details + newDetailContexts)
+ )
+ Disposer.register(states.popup, updatedStates)
+ CodeWhispererPopupManager.getInstance().initPopupListener(updatedStates)
+ return updatedStates
}
- private fun checkRecommendationsValidity(states: InvocationContext?, showHint: Boolean): Boolean {
- if (states == null) return false
+ private fun checkRecommendationsValidity(states: InvocationContext, showHint: Boolean): Boolean {
val details = states.recommendationContext.details
// set to true when at least one is not discarded or empty
@@ -643,48 +614,35 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
return hasAtLeastOneValid
}
- private fun updateCodeWhisperer(sessionContext: SessionContext, states: InvocationContext, recommendationAdded: Boolean) {
- CodeWhispererPopupManager.getInstance().changeStatesForShowing(sessionContext, states, recommendationAdded)
- }
-
- @RequiresEdt
- private fun disposeJob(jobId: Int) {
- ongoingRequests[jobId]?.let { Disposer.dispose(it) }
- ongoingRequests.remove(jobId)
- ongoingRequestsContext.remove(jobId)
+ private fun updateCodeWhisperer(states: InvocationContext, recommendationAdded: Boolean) {
+ CodeWhispererPopupManager.getInstance().changeStates(states, 0, "", true, recommendationAdded)
}
- @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)
- }
- sessionContext = null
- val jobIds = ongoingRequests.keys.toList()
- jobIds.forEach { jobId -> disposeJob(jobId) }
- ongoingRequests.clear()
- ongoingRequestsContext.clear()
- }
-
- fun getAllSuggestionsPreviewInfo() =
- ongoingRequests.values.filterNotNull().flatMap { element ->
- val context = element.recommendationContext
- context.details.map {
- PreviewContext(context.jobId, it, context.userInputSinceInvocation, context.typeahead)
- }
+ private fun sendDiscardedUserDecisionEventForAll(
+ requestContext: RequestContext,
+ responseContext: ResponseContext,
+ recommendations: List,
+ ) {
+ val detailContexts = recommendations.map {
+ DetailContext("", it, it, true, false, "", getCompletionType(it))
}
+ val recommendationContext = RecommendationContext(detailContexts, "", "", VisualPosition(0, 0))
- fun getAllPaginationSessions() = ongoingRequests
+ CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
+ requestContext,
+ responseContext,
+ recommendationContext,
+ SessionContext(),
+ false
+ )
+ }
fun getRequestContext(
triggerTypeInfo: TriggerTypeInfo,
editor: Editor,
project: Project,
psiFile: PsiFile,
+ latencyContext: LatencyContext,
): RequestContext {
// 1. file context
val fileContext: FileContextInfo = runReadAction { FileContextProvider.getInstance(project).extractFileContext(editor, psiFile) }
@@ -709,7 +667,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, customizationArn)
+ return RequestContext(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, latencyContext, customizationArn)
}
fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse {
@@ -735,19 +693,28 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
requestContext: RequestContext,
responseContext: ResponseContext,
recommendationContext: RecommendationContext,
+ popup: JBPopup,
): 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)
- Disposer.register(states) {
- job?.cancel(CancellationException("Cancelling the current coroutine when the pagination session context is disposed"))
- }
+ 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()
+ }
+ }
+
private fun logServiceInvocation(
requestId: String,
requestContext: RequestContext,
@@ -767,8 +734,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
"Left context of current line: ${requestContext.fileContextInfo.caretContext.leftContextOnCurrentLine}, " +
"Cursor line: ${requestContext.caretPosition.line}, " +
"Caret offset: ${requestContext.caretPosition.offset}, " +
- latency?.let { "Latency: $latency, " }.orEmpty() +
- exceptionType?.let { "Exception Type: $it, " }.orEmpty() +
+ (latency?.let { "Latency: $latency, " } ?: "") +
+ (exceptionType?.let { "Exception Type: $it, " } ?: "") +
"Recommendations: \n${recommendationLogs ?: "None"}"
}
}
@@ -785,19 +752,28 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
return false
}
- if (CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive()) {
- LOG.debug { "Find an existing CodeWhisperer session before triggering CodeWhisperer, not invoking service" }
+ 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" }
return false
}
return true
}
- fun showCodeWhispererInfoHint(editor: Editor, message: String) {
- HintManager.getInstance().showInformationHint(editor, message, HintManager.UNDER)
+ private fun showCodeWhispererInfoHint(editor: Editor, message: String) {
+ runInEdt {
+ HintManager.getInstance().showInformationHint(editor, message, HintManager.UNDER)
+ }
}
- fun showCodeWhispererErrorHint(editor: Editor, message: String) {
- HintManager.getInstance().showErrorHint(editor, message, HintManager.UNDER)
+ private fun showCodeWhispererErrorHint(editor: Editor, message: String) {
+ runInEdt {
+ HintManager.getInstance().showErrorHint(editor, message, HintManager.UNDER)
+ }
}
override fun dispose() {}
@@ -805,19 +781,12 @@ 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"
private var reAuthPromptShown = false
@@ -865,10 +834,42 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
}
}
-interface CodeWhispererIntelliSenseOnHoverListener {
- fun onEnter() {}
+data class RequestContext(
+ val project: Project,
+ val editor: Editor,
+ val triggerTypeInfo: TriggerTypeInfo,
+ val caretPosition: CaretPosition,
+ 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
+ var supplementalContext: SupplementalContextInfo? = null
+ private set
+ get() = when (field) {
+ null -> {
+ if (!supplementalContextDeferred.isCompleted) {
+ error("attempt to access supplemental context before awaiting the deferred")
+ } else {
+ null
+ }
+ }
+
+ else -> field
+ }
+
+ suspend fun awaitSupplementalContext(): SupplementalContextInfo? {
+ supplementalContext = supplementalContextDeferred.await()
+ return supplementalContext
+ }
}
+data class ResponseContext(
+ val sessionId: String,
+)
+
interface CodeWhispererCodeCompletionServiceListener {
fun onSuccess(fileContextInfo: FileContextInfo) {}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
new file mode 100644
index 0000000000..7f43e3d088
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
@@ -0,0 +1,898 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.service
+
+import com.intellij.codeInsight.hint.HintManager
+import com.intellij.notification.NotificationAction
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.ApplicationInfo
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.VisualPosition
+import com.intellij.openapi.project.Project
+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 kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import software.amazon.awssdk.core.exception.SdkServiceException
+import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList
+import software.amazon.awssdk.services.codewhisperer.model.CodeWhispererException
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.amazon.awssdk.services.codewhispererruntime.model.Completion
+import software.amazon.awssdk.services.codewhispererruntime.model.FileContext
+import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage
+import software.amazon.awssdk.services.codewhispererruntime.model.RecommendationsWithReferencesPreference
+import software.amazon.awssdk.services.codewhispererruntime.model.ResourceNotFoundException
+import software.amazon.awssdk.services.codewhispererruntime.model.SupplementalContext
+import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
+import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
+import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
+import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.getCaretPosition
+import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.isSupportedJsonFormat
+import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJson
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.WorkerContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService.Companion.CODEWHISPERER_CODE_COMPLETION_PERFORMED
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeInsightsSettingsFacade
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.jetbrains.utils.isInjectedText
+import software.aws.toolkits.jetbrains.utils.isQExpired
+import software.aws.toolkits.jetbrains.utils.notifyWarn
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.CodewhispererCompletionType
+import software.aws.toolkits.telemetry.CodewhispererSuggestionState
+import software.aws.toolkits.telemetry.CodewhispererTriggerType
+import java.util.concurrent.TimeUnit
+
+@Service
+class CodeWhispererServiceNew(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: SessionContextNew? = null
+
+ init {
+ Disposer.register(this, codeInsightSettingsFacade)
+ }
+
+ private var job: Job? = null
+ fun showRecommendationsInPopup(
+ editor: Editor,
+ triggerTypeInfo: TriggerTypeInfo,
+ latencyContext: LatencyContext,
+ ): Job? {
+ if (job == null || job?.isCompleted == true) {
+ job = cs.launch(getCoroutineBgContext()) {
+ doShowRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
+ }
+ }
+
+ // did some wrangling, but compiler didn't believe this can't be null
+ return job
+ }
+
+ private suspend fun doShowRecommendationsInPopup(
+ editor: Editor,
+ triggerTypeInfo: TriggerTypeInfo,
+ latencyContext: LatencyContext,
+ ) {
+ val project = editor.project ?: return
+ if (!isCodeWhispererEnabled(project)) return
+
+ latencyContext.credentialFetchingStart = System.nanoTime()
+
+ // try to refresh automatically if possible, otherwise ask user to login again
+ if (isQExpired(project)) {
+ // consider changing to only running once a ~minute since this is relatively expensive
+ // say the connection is un-refreshable if refresh fails for 3 times
+ val shouldReauth = if (refreshFailure < MAX_REFRESH_ATTEMPT) {
+ val attempt = withContext(getCoroutineBgContext()) {
+ promptReAuth(project)
+ }
+
+ if (!attempt) {
+ refreshFailure++
+ }
+
+ attempt
+ } else {
+ true
+ }
+
+ if (shouldReauth) {
+ return
+ }
+ }
+
+ latencyContext.credentialFetchingEnd = System.nanoTime()
+ val psiFile = runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(editor.document) }
+
+ if (psiFile == null) {
+ LOG.debug { "No PSI file for the current document" }
+ if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ showCodeWhispererInfoHint(editor, message("codewhisperer.trigger.document.unsupported"))
+ }
+ return
+ }
+ val isInjectedFile = runReadAction { psiFile.isInjectedText() }
+ if (isInjectedFile) return
+
+ val currentJobId = jobId++
+ val requestContext = try {
+ getRequestContext(triggerTypeInfo, editor, project, psiFile)
+ } catch (e: Exception) {
+ LOG.debug { e.message.toString() }
+ CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName)
+ return
+ }
+ val caretContext = requestContext.fileContextInfo.caretContext
+ 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() || (
+ language is CodeWhispererJson && !isSupportedJsonFormat(
+ requestContext.fileContextInfo.filename,
+ leftContext
+ )
+ )
+ ) {
+ LOG.debug { "Programming language $language is not supported by CodeWhisperer" }
+ if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ showCodeWhispererInfoHint(
+ requestContext.editor,
+ message("codewhisperer.language.error", psiFile.fileType.name)
+ )
+ }
+ return
+ }
+
+ LOG.debug {
+ "Calling CodeWhisperer service, jobId: $currentJobId, trigger type: ${triggerTypeInfo.triggerType}" +
+ if (triggerTypeInfo.triggerType == CodewhispererTriggerType.AutoTrigger) {
+ ", auto-trigger type: ${triggerTypeInfo.automatedTriggerType}"
+ } else {
+ ""
+ }
+ }
+
+ CodeWhispererInvocationStatusNew.getInstance().startInvocation()
+
+ invokeCodeWhispererInBackground(requestContext, currentJobId, latencyContext)
+ }
+
+ internal suspend fun invokeCodeWhispererInBackground(requestContext: RequestContextNew, currentJobId: Int, latencyContext: LatencyContext) {
+ ongoingRequestsContext[currentJobId] = requestContext
+ val sessionContext = sessionContext ?: SessionContextNew(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
+ }
+ this.sessionContext = sessionContext
+
+ val workerContexts = mutableListOf()
+
+ // When session is disposed we will cancel this coroutine. The only places session can get disposed should be
+ // from CodeWhispererService.disposeDisplaySession().
+ // 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.
+ var lastRecommendationIndex = -1
+
+ try {
+ val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator(
+ buildCodeWhispererRequest(
+ requestContext.fileContextInfo,
+ requestContext.awaitSupplementalContext(),
+ requestContext.customizationArn
+ )
+ )
+
+ var startTime = System.nanoTime()
+ latencyContext.codewhispererPreprocessingEnd = System.nanoTime()
+ latencyContext.paginationAllCompletionsStart = System.nanoTime()
+ CodeWhispererInvocationStatusNew.getInstance().setInvocationStart()
+ var requestCount = 0
+ for (response in responseIterable) {
+ requestCount++
+ val endTime = System.nanoTime()
+ val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble()
+ startTime = endTime
+ val requestId = response.responseMetadata().requestId()
+ val sessionId = response.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ if (requestCount == 1) {
+ latencyContext.codewhispererPostprocessingStart = System.nanoTime()
+ latencyContext.paginationFirstCompletionTime = latency
+ latencyContext.firstRequestId = requestId
+ CodeWhispererInvocationStatusNew.getInstance().setInvocationSessionId(sessionId)
+ }
+ if (response.nextToken().isEmpty()) {
+ latencyContext.paginationAllCompletionsEnd = System.nanoTime()
+ }
+ val responseContext = ResponseContext(sessionId)
+ logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null)
+ lastRecommendationIndex += response.completions().size
+ ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED)
+ .onSuccess(requestContext.fileContextInfo)
+ CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent(
+ currentJobId,
+ requestId,
+ requestContext,
+ responseContext,
+ lastRecommendationIndex,
+ true,
+ latency,
+ null
+ )
+
+ val validatedResponse = validateResponse(response)
+
+ runInEdt {
+ // If delay is not met, add them to the worker queue and process them later.
+ // On first response, workers queue must be empty. If there's enough delay before showing,
+ // process CodeWhisperer UI rendering and workers queue will remain empty throughout this
+ // CodeWhisperer session. If there's not enough delay before showing, the CodeWhisperer UI rendering task
+ // will be added to the workers queue.
+ // On subsequent responses, if they see workers queue is not empty, it means the first worker
+ // task hasn't been finished yet, in this case simply add another task to the queue. If they
+ // see worker queue is empty, the previous tasks must have been finished before this. In this
+ // case render CodeWhisperer UI directly.
+ val workerContext = WorkerContextNew(requestContext, responseContext, validatedResponse)
+ if (workerContexts.isNotEmpty()) {
+ workerContexts.add(workerContext)
+ } else {
+ if (ongoingRequests.values.filterNotNull().isEmpty() &&
+ !CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer()
+ ) {
+ // It's the first response, and no enough delay before showing
+ projectCoroutineScope(requestContext.project).launch {
+ while (!CodeWhispererInvocationStatusNew.getInstance().hasEnoughDelayToShowCodeWhisperer()) {
+ delay(CodeWhispererConstants.POPUP_DELAY_CHECK_INTERVAL)
+ }
+ runInEdt {
+ workerContexts.forEach {
+ processCodeWhispererUI(
+ sessionContext,
+ it,
+ ongoingRequests[currentJobId],
+ cs,
+ currentJobId
+ )
+ if (!ongoingRequests.contains(currentJobId)) {
+ job?.cancel()
+ }
+ }
+ workerContexts.clear()
+ }
+ }
+ workerContexts.add(workerContext)
+ } else {
+ // Have enough delay before showing for the first response, or it's subsequent responses
+ processCodeWhispererUI(
+ sessionContext,
+ workerContext,
+ ongoingRequests[currentJobId],
+ cs,
+ currentJobId
+ )
+ if (!ongoingRequests.contains(currentJobId)) {
+ job?.cancel()
+ }
+ }
+ }
+ }
+ if (!cs.isActive) {
+ // If job is cancelled before we do another request, don't bother making
+ // another API call to save resources
+ LOG.debug { "Skipping sending remaining requests on inactive CodeWhisperer session exit" }
+ return
+ }
+ if (requestCount >= PAGINATION_REQUEST_COUNT_ALLOWED) {
+ LOG.debug { "Only $PAGINATION_REQUEST_COUNT_ALLOWED request per pagination session for now" }
+ CodeWhispererInvocationStatusNew.getInstance().finishInvocation()
+ break
+ }
+ }
+ } catch (e: Exception) {
+ val requestId: String
+ val sessionId: String
+ val displayMessage: String
+
+ if (
+ CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) ||
+ e is ResourceNotFoundException
+ ) {
+ (e as CodeWhispererRuntimeException)
+
+ requestId = e.requestId().orEmpty()
+ sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ val exceptionType = e::class.simpleName
+ val responseContext = ResponseContext(sessionId)
+
+ CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent(
+ currentJobId,
+ requestId,
+ requestContext,
+ responseContext,
+ lastRecommendationIndex,
+ false,
+ 0.0,
+ exceptionType
+ )
+
+ LOG.debug {
+ "The provided customization ${requestContext.customizationArn} is not found, " +
+ "will fallback to the default and retry generate completion"
+ }
+ logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
+
+ notifyWarn(
+ title = "",
+ content = message("codewhisperer.notification.custom.not_available"),
+ project = requestContext.project,
+ notificationActions = listOf(
+ NotificationAction.create(
+ message("codewhisperer.notification.custom.simple.button.select_another_customization")
+ ) { _, notification ->
+ CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project)
+ notification.expire()
+ }
+ )
+ )
+ CodeWhispererInvocationStatusNew.getInstance().finishInvocation()
+
+ requestContext.customizationArn?.let { CodeWhispererModelConfigurator.getInstance().invalidateCustomization(it) }
+
+ showRecommendationsInPopup(
+ requestContext.editor,
+ requestContext.triggerTypeInfo,
+ latencyContext
+ )
+ return
+ } else if (e is CodeWhispererException) {
+ requestId = e.requestId().orEmpty()
+ sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side")
+ } else if (e is CodeWhispererRuntimeException) {
+ requestId = e.requestId().orEmpty()
+ sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
+ displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side")
+ } else {
+ requestId = ""
+ sessionId = ""
+ val statusCode = if (e is SdkServiceException) e.statusCode() else 0
+ displayMessage =
+ if (statusCode >= 500) {
+ message("codewhisperer.trigger.error.server_side")
+ } else {
+ message("codewhisperer.trigger.error.client_side")
+ }
+ if (statusCode < 500) {
+ LOG.debug(e) { "Error invoking CodeWhisperer service" }
+ }
+ }
+ val exceptionType = e::class.simpleName
+ val responseContext = ResponseContext(sessionId)
+ CodeWhispererInvocationStatusNew.getInstance().setInvocationSessionId(sessionId)
+ logServiceInvocation(requestId, requestContext, responseContext, emptyList(), null, exceptionType)
+ CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent(
+ currentJobId,
+ requestId,
+ requestContext,
+ responseContext,
+ lastRecommendationIndex,
+ false,
+ 0.0,
+ exceptionType
+ )
+
+ if (e is ThrottlingException &&
+ e.message == CodeWhispererConstants.THROTTLING_MESSAGE
+ ) {
+ CodeWhispererExplorerActionManager.getInstance().setSuspended(requestContext.project)
+ if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ notifyErrorCodeWhispererUsageLimit(requestContext.project)
+ }
+ } else {
+ if (requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ // We should only show error hint when CodeWhisperer popup is not visible,
+ // and make it silent if CodeWhisperer popup is showing.
+ runInEdt {
+ if (!CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) {
+ showCodeWhispererErrorHint(requestContext.editor, displayMessage)
+ }
+ }
+ }
+ }
+ CodeWhispererInvocationStatusNew.getInstance().finishInvocation()
+ runInEdt {
+ CodeWhispererPopupManagerNew.getInstance().updatePopupPanel(sessionContext)
+ }
+ }
+ }
+
+ @RequiresEdt
+ private fun processCodeWhispererUI(
+ sessionContext: SessionContextNew,
+ workerContext: WorkerContextNew,
+ currStates: InvocationContextNew?,
+ coroutine: CoroutineScope,
+ jobId: Int,
+ ) {
+ val requestContext = workerContext.requestContext
+ val responseContext = workerContext.responseContext
+ val response = workerContext.response
+ 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 (!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 all CodeWhisperer recommendations since editor is disposed. RequestId: $requestId, jobId: $jobId" }
+ disposeDisplaySession(false)
+ return
+ }
+
+ CodeWhispererInvocationStatusNew.getInstance().finishInvocation()
+
+ val caretMovement = CodeWhispererEditorManagerNew.getInstance().getCaretMovement(
+ requestContext.editor,
+ requestContext.caretPosition
+ )
+ val isPopupShowing = checkRecommendationsValidity(currStates, false)
+ val nextStates: InvocationContextNew?
+ if (currStates == null) {
+ // first response for the jobId
+ nextStates = initStates(jobId, requestContext, responseContext, response, caretMovement)
+
+ // receiving a null state means caret has moved backward,
+ // so we are going to cancel the current job
+ if (nextStates == null) {
+ return
+ }
+ } else {
+ // subsequent responses for the jobId
+ nextStates = updateStates(currStates, response)
+ }
+ LOG.debug { "Adding ${response.completions().size} completions to the session. RequestId: $requestId, jobId: $jobId" }
+
+ // 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.count { !it.isDiscarded }
+ 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
+ // TODO: may have bug; visit later
+ if (nextStates.recommendationContext.details.isEmpty()) {
+ LOG.debug { "Received just an empty list from this session, requestId: $requestId" }
+ CodeWhispererTelemetryServiceNew.getInstance().sendUserDecisionEvent(
+ requestContext,
+ responseContext,
+ DetailContextNew(
+ requestId,
+ Completion.builder().build(),
+ Completion.builder().build(),
+ false,
+ false,
+ "",
+ CodewhispererCompletionType.Line
+ ),
+ -1,
+ CodewhispererSuggestionState.Empty,
+ nextStates.recommendationContext.details.size
+ )
+ }
+ if (!hasAtLeastOneValid) {
+ 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 = CodeWhispererPopupManagerNew.getInstance().findNewSelectedIndex(true, sessionContext.selectedIndex)
+ }
+ } else {
+ updateCodeWhisperer(sessionContext, nextStates, isPopupShowing)
+ }
+ }
+
+ private fun initStates(
+ jobId: Int,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ response: GenerateCompletionsResponse,
+ caretMovement: CaretMovement,
+ ): InvocationContextNew? {
+ val requestId = response.responseMetadata().requestId()
+ val recommendations = response.completions()
+ val visualPosition = requestContext.editor.caretModel.visualPosition
+
+ if (caretMovement == CaretMovement.MOVE_BACKWARD) {
+ LOG.debug { "Caret moved backward, discarding all of the recommendations and exiting the session. Request ID: $requestId, jobId: $jobId" }
+ val detailContexts = recommendations.map {
+ DetailContextNew("", it, it, true, false, "", getCompletionType(it))
+ }.toMutableList()
+ val recommendationContext = RecommendationContextNew(detailContexts, "", "", VisualPosition(0, 0), jobId)
+ ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext)
+ disposeDisplaySession(false)
+ return null
+ }
+
+ val userInputOriginal = CodeWhispererEditorManagerNew.getInstance().getUserInputSinceInvocation(
+ requestContext.editor,
+ requestContext.caretPosition.offset
+ )
+ val userInput =
+ if (caretMovement == CaretMovement.NO_CHANGE) {
+ LOG.debug { "Caret position not changed since invocation. Request ID: $requestId" }
+ ""
+ } else {
+ userInputOriginal.trimStart().also {
+ LOG.debug {
+ "Caret position moved forward since invocation. Request ID: $requestId, " +
+ "user input since invocation: $userInputOriginal, " +
+ "user input without leading spaces: $it"
+ }
+ }
+ }
+ val detailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext(
+ requestContext,
+ userInput,
+ recommendations,
+ requestId
+ )
+ val recommendationContext = RecommendationContextNew(detailContexts, userInputOriginal, userInput, visualPosition, jobId)
+ ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext)
+ return ongoingRequests[jobId]
+ }
+
+ private fun updateStates(
+ states: InvocationContextNew,
+ response: GenerateCompletionsResponse,
+ ): InvocationContextNew {
+ val recommendationContext = states.recommendationContext
+ val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext(
+ states.requestContext,
+ recommendationContext.userInputSinceInvocation,
+ response.completions(),
+ response.responseMetadata().requestId()
+ )
+
+ recommendationContext.details.addAll(newDetailContexts)
+ return states
+ }
+
+ private fun checkRecommendationsValidity(states: InvocationContextNew?, 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
+ val hasAtLeastOneValid = details.any { !it.isDiscarded && it.recommendation.content().isNotEmpty() }
+
+ if (!hasAtLeastOneValid && showHint && states.requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) {
+ showCodeWhispererInfoHint(
+ states.requestContext.editor,
+ message("codewhisperer.popup.no_recommendations")
+ )
+ }
+ return hasAtLeastOneValid
+ }
+
+ private fun updateCodeWhisperer(sessionContext: SessionContextNew, states: InvocationContextNew, recommendationAdded: Boolean) {
+ CodeWhispererPopupManagerNew.getInstance().changeStatesForShowing(sessionContext, states, recommendationAdded)
+ }
+
+ @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)
+ }
+ sessionContext = null
+ val jobIds = ongoingRequests.keys.toList()
+ jobIds.forEach { jobId -> disposeJob(jobId) }
+ ongoingRequests.clear()
+ ongoingRequestsContext.clear()
+ }
+
+ 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 getRequestContext(
+ triggerTypeInfo: TriggerTypeInfo,
+ editor: Editor,
+ project: Project,
+ psiFile: PsiFile,
+ ): RequestContextNew {
+ // 1. file context
+ val fileContext: FileContextInfo = runReadAction { FileContextProvider.getInstance(project).extractFileContext(editor, psiFile) }
+
+ // the upper bound for supplemental context duration is 50ms
+ // 2. supplemental context
+ val supplementalContext = cs.async {
+ try {
+ FileContextProvider.getInstance(project).extractSupplementalFileContext(psiFile, fileContext, timeout = SUPPLEMENTAL_CONTEXT_TIMEOUT)
+ } catch (e: Exception) {
+ LOG.warn { "Run into unexpected error when fetching supplemental context, error: ${e.message}" }
+ null
+ }
+ }
+
+ // 3. caret position
+ val caretPosition = runReadAction { getCaretPosition(editor) }
+
+ // 4. connection
+ val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(CodeWhispererConnection.getInstance())
+
+ // 5. customization
+ val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn
+
+ return RequestContextNew(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, customizationArn)
+ }
+
+ fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse {
+ // If contentSpans in reference are not consistent with content(recommendations),
+ // remove the incorrect references.
+ val validatedRecommendations = response.completions().map {
+ val validReferences = it.hasReferences() && it.references().isNotEmpty() &&
+ it.references().none { reference ->
+ val span = reference.recommendationContentSpan()
+ span.start() > span.end() || span.start() < 0 || span.end() > it.content().length
+ }
+ if (validReferences) {
+ it
+ } else {
+ it.toBuilder().references(DefaultSdkAutoConstructList.getInstance()).build()
+ }
+ }
+
+ return response.toBuilder().completions(validatedRecommendations).build()
+ }
+
+ private fun buildInvocationContext(
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ recommendationContext: RecommendationContextNew,
+ ): InvocationContextNew {
+ // 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 = InvocationContextNew(requestContext, responseContext, recommendationContext)
+ Disposer.register(states) {
+ job?.cancel(CancellationException("Cancelling the current coroutine when the pagination session context is disposed"))
+ }
+ return states
+ }
+
+ private fun logServiceInvocation(
+ requestId: String,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ recommendations: List,
+ latency: Double?,
+ exceptionType: String?,
+ ) {
+ val recommendationLogs = recommendations.map { it.content().trimEnd() }
+ .reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" }
+ LOG.info {
+ "SessionId: ${responseContext.sessionId}, " +
+ "RequestId: $requestId, " +
+ "Jetbrains IDE: ${ApplicationInfo.getInstance().fullApplicationName}, " +
+ "IDE version: ${ApplicationInfo.getInstance().apiVersion}, " +
+ "Filename: ${requestContext.fileContextInfo.filename}, " +
+ "Left context of current line: ${requestContext.fileContextInfo.caretContext.leftContextOnCurrentLine}, " +
+ "Cursor line: ${requestContext.caretPosition.line}, " +
+ "Caret offset: ${requestContext.caretPosition.offset}, " +
+ latency?.let { "Latency: $latency, " }.orEmpty() +
+ exceptionType?.let { "Exception Type: $it, " }.orEmpty() +
+ "Recommendations: \n${recommendationLogs ?: "None"}"
+ }
+ }
+
+ fun canDoInvocation(editor: Editor, type: CodewhispererTriggerType): Boolean {
+ editor.project?.let {
+ if (!isCodeWhispererEnabled(it)) {
+ return false
+ }
+ }
+
+ if (type == CodewhispererTriggerType.AutoTrigger && !CodeWhispererExplorerActionManager.getInstance().isAutoEnabled()) {
+ LOG.debug { "CodeWhisperer auto-trigger is disabled, not invoking service" }
+ return false
+ }
+
+ if (CodeWhispererInvocationStatusNew.getInstance().isDisplaySessionActive()) {
+ LOG.debug { "Find an existing CodeWhisperer session before triggering CodeWhisperer, not invoking service" }
+ return false
+ }
+ return true
+ }
+
+ fun showCodeWhispererInfoHint(editor: Editor, message: String) {
+ HintManager.getInstance().showInformationHint(editor, message, HintManager.UNDER)
+ }
+
+ fun showCodeWhispererErrorHint(editor: Editor, message: String) {
+ HintManager.getInstance().showErrorHint(editor, message, HintManager.UNDER)
+ }
+
+ override fun dispose() {}
+
+ companion object {
+ private val LOG = getLogger()
+ private const val MAX_REFRESH_ATTEMPT = 3
+ private const val PAGINATION_REQUEST_COUNT_ALLOWED = 1
+
+ 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(): CodeWhispererServiceNew = service()
+ const val KET_SESSION_ID = "x-amzn-SessionId"
+ private var reAuthPromptShown = false
+
+ fun markReAuthPromptShown() {
+ reAuthPromptShown = true
+ }
+
+ fun hasReAuthPromptBeenShown() = reAuthPromptShown
+
+ fun buildCodeWhispererRequest(
+ fileContextInfo: FileContextInfo,
+ supplementalContext: SupplementalContextInfo?,
+ customizationArn: String?,
+ ): GenerateCompletionsRequest {
+ val programmingLanguage = ProgrammingLanguage.builder()
+ .languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId)
+ .build()
+ val fileContext = FileContext.builder()
+ .leftFileContent(fileContextInfo.caretContext.leftFileContext)
+ .rightFileContent(fileContextInfo.caretContext.rightFileContext)
+ .filename(fileContextInfo.fileRelativePath ?: fileContextInfo.filename)
+ .programmingLanguage(programmingLanguage)
+ .build()
+ val supplementalContexts = supplementalContext?.contents?.map {
+ SupplementalContext.builder()
+ .content(it.content)
+ .filePath(it.path)
+ .build()
+ }.orEmpty()
+ val includeCodeWithReference = if (CodeWhispererSettings.getInstance().isIncludeCodeWithReference()) {
+ RecommendationsWithReferencesPreference.ALLOW
+ } else {
+ RecommendationsWithReferencesPreference.BLOCK
+ }
+
+ return GenerateCompletionsRequest.builder()
+ .fileContext(fileContext)
+ .supplementalContexts(supplementalContexts)
+ .referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) }
+ .customizationArn(customizationArn)
+ .optOutPreference(getTelemetryOptOutPreference())
+ .build()
+ }
+ }
+}
+
+data class RequestContextNew(
+ val project: Project,
+ val editor: Editor,
+ val triggerTypeInfo: TriggerTypeInfo,
+ val caretPosition: CaretPosition,
+ val fileContextInfo: FileContextInfo,
+ private val supplementalContextDeferred: Deferred,
+ val connection: ToolkitConnection?,
+ val customizationArn: String?,
+) {
+ // TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only
+ var supplementalContext: SupplementalContextInfo? = null
+ private set
+ get() = when (field) {
+ null -> {
+ if (!supplementalContextDeferred.isCompleted) {
+ error("attempt to access supplemental context before awaiting the deferred")
+ } else {
+ null
+ }
+ }
+ else -> field
+ }
+
+ suspend fun awaitSupplementalContext(): SupplementalContextInfo? {
+ supplementalContext = supplementalContextDeferred.await()
+ return supplementalContext
+ }
+}
+
+interface CodeWhispererIntelliSenseOnHoverListener {
+ fun onEnter() {}
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
index dc877fd3e6..8aaf55a717 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
@@ -17,6 +17,7 @@ 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.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
@@ -94,18 +95,20 @@ class CodeWhispererConfigurable(private val project: Project) :
}.comment(message("aws.settings.codewhisperer.automatic_import_adder.tooltip"))
}
- row {
- link("Configure inline suggestion keybindings") { e ->
- // TODO: user needs feedback if these are null
- 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)
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ row {
+ link("Configure inline suggestion keybindings") { e ->
+ // TODO: user needs feedback if these are null
+ val settings = DataManager.getInstance().getDataContext(e.source as ActionLink).getData(Settings.KEY) ?: return@link
+ val configurable: Configurable = settings.find("preferences.keymap") ?: return@link
- // 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)
+
+ // 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)
+ }
}
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt
index ce5de79f1f..f182197658 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererIntelliSenseAutoTriggerListener.kt
@@ -11,9 +11,10 @@ import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.observable.util.addMouseHoverListener
import com.intellij.ui.hover.HoverListener
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
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.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
import java.awt.Component
object CodeWhispererIntelliSenseAutoTriggerListener : LookupManagerListener {
@@ -32,7 +33,6 @@ object CodeWhispererIntelliSenseAutoTriggerListener : LookupManagerListener {
CodeWhispererAutoTriggerService.getInstance().tryInvokeAutoTrigger(editor, CodeWhispererAutomatedTriggerType.IntelliSense())
cleanup()
}
-
override fun lookupCanceled(event: LookupEvent) {
cleanup()
}
@@ -42,20 +42,21 @@ object CodeWhispererIntelliSenseAutoTriggerListener : LookupManagerListener {
}
})
- (newLookup as LookupImpl).component.addMouseHoverListener(
- newLookup,
- object : HoverListener() {
- override fun mouseEntered(component: Component, x: Int, y: Int) {
- runReadAction {
- newLookup.project.messageBus.syncPublisher(
- CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER,
- ).onEnter()
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ (newLookup as LookupImpl).component.addMouseHoverListener(
+ newLookup,
+ object : HoverListener() {
+ override fun mouseEntered(component: Component, x: Int, y: Int) {
+ runReadAction {
+ newLookup.project.messageBus.syncPublisher(
+ CodeWhispererServiceNew.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER,
+ ).onEnter()
+ }
}
+ override fun mouseMoved(component: Component, x: Int, y: Int) {}
+ override fun mouseExited(component: Component) {}
}
-
- override fun mouseMoved(component: Component, x: Int, y: Int) {}
- override fun mouseExited(component: Component) {}
- }
- )
+ )
+ }
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
index b7f7c8b333..2e42b47f65 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
@@ -21,12 +21,14 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileWatcher
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.QActionGroups.Q_SIGNED_OUT_ACTION_GROUP
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomizationListener
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.QStatusBarLoggedInActionGroup
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStateChangeListener
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.reconnectCodeWhisperer
import software.aws.toolkits.jetbrains.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
@@ -123,7 +125,13 @@ class CodeWhispererStatusBarWidget(project: Project) :
AllIcons.General.BalloonWarning
} else if (!isQConnected(project)) {
AllIcons.RunConfigurations.TestState.Run
- } else if (CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation()) {
+ } else if (
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ CodeWhispererInvocationStatusNew.getInstance().hasExistingServiceInvocation()
+ } else {
+ CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation()
+ }
+ ) {
// AnimatedIcon can't serialize over remote host
if (!AppMode.isRemoteDevHost()) {
AnimatedIcon.Default()
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt
index e5f0e504be..dc512c1ca9 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt
@@ -19,13 +19,16 @@ import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererCodeCompletionServiceListener
@@ -86,30 +89,52 @@ abstract class CodeWhispererCodeCoverageTracker(
if (!isTelemetryEnabled() || isActive.getAndSet(true)) return
val conn = ApplicationManager.getApplication().messageBus.connect()
- conn.subscribe(
- CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
- object : CodeWhispererUserActionListener {
- override fun afterAccept(
- states: InvocationContext,
- previews: List,
- sessionContext: SessionContext,
- rangeMarker: RangeMarker,
- ) {
- if (states.requestContext.fileContextInfo.programmingLanguage != language) return
- rangeMarkers.add(rangeMarker)
- val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return
- rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation)
- runReadAction {
- // also increment total tokens because accepted tokens are part of it
- incrementTotalCharsCount(rangeMarker.document, originalRecommendation.length)
- // avoid counting CodeWhisperer inserted suggestion twice in total tokens
- if (rangeMarker.textRange.length in 2..49 && originalRecommendation.trim().isNotEmpty()) {
- incrementTotalCharsCount(rangeMarker.document, -rangeMarker.textRange.length)
+ if (CodeWhispererFeatureConfigService.getInstance().getNewAutoTriggerUX()) {
+ conn.subscribe(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
+ object : CodeWhispererUserActionListener {
+ override fun afterAccept(
+ states: InvocationContextNew,
+ previews: List,
+ sessionContext: SessionContextNew,
+ rangeMarker: RangeMarker,
+ ) {
+ if (states.requestContext.fileContextInfo.programmingLanguage != language) return
+ rangeMarkers.add(rangeMarker)
+ val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return
+ rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation)
+ runReadAction {
+ // also increment total tokens because accepted tokens are part of it
+ incrementTotalCharsCount(rangeMarker.document, originalRecommendation.length)
+ // avoid counting CodeWhisperer inserted suggestion twice in total tokens
+ if (rangeMarker.textRange.length in 2..49 && originalRecommendation.trim().isNotEmpty()) {
+ incrementTotalCharsCount(rangeMarker.document, -rangeMarker.textRange.length)
+ }
}
}
}
- }
- )
+ )
+ } else {
+ conn.subscribe(
+ CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
+ object : CodeWhispererUserActionListener {
+ override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {
+ if (states.requestContext.fileContextInfo.programmingLanguage != language) return
+ rangeMarkers.add(rangeMarker)
+ val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return
+ rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation)
+ runReadAction {
+ // also increment total tokens because accepted tokens are part of it
+ incrementTotalCharsCount(rangeMarker.document, originalRecommendation.length)
+ // avoid counting CodeWhisperer inserted suggestion twice in total tokens
+ if (rangeMarker.textRange.length in 2..49 && originalRecommendation.trim().isNotEmpty()) {
+ incrementTotalCharsCount(rangeMarker.document, -rangeMarker.textRange.length)
+ }
+ }
+ }
+ }
+ )
+ }
conn.subscribe(
CodeWhispererService.CODEWHISPERER_CODE_COMPLETION_PERFORMED,
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 adda99e442..77f62b65b9 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,7 +14,6 @@ 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
@@ -24,14 +23,14 @@ 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.RequestContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
-import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl
@@ -60,9 +59,11 @@ class CodeWhispererTelemetryService {
private var previousUserTriggerDecisionTimestamp: Instant? = null
- private val codewhispererTimeSinceLastUserDecision: Double? =
- previousUserTriggerDecisionTimestamp?.let {
- Duration.between(it, Instant.now()).toMillis().toDouble()
+ private val codewhispererTimeSinceLastUserDecision: Double?
+ get() {
+ return previousUserTriggerDecisionTimestamp?.let {
+ Duration.between(it, Instant.now()).toMillis().toDouble()
+ }
}
val previousUserTriggerDecision: CodewhispererPreviousSuggestionState?
@@ -89,7 +90,6 @@ class CodeWhispererTelemetryService {
}
fun sendServiceInvocationEvent(
- jobId: Int,
requestId: String,
requestContext: RequestContext,
responseContext: ResponseContext,
@@ -98,7 +98,6 @@ class CodeWhispererTelemetryService {
latency: Double,
exceptionType: String?,
) {
- LOG.debug { "Sending serviceInvocation for $requestId, jobId: $jobId" }
val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo
val (offset, line) = requestContext.caretPosition
@@ -187,7 +186,6 @@ class CodeWhispererTelemetryService {
}
fun sendUserTriggerDecisionEvent(
- sessionContext: SessionContext,
requestContext: RequestContext,
responseContext: ResponseContext,
recommendationContext: RecommendationContext,
@@ -224,7 +222,6 @@ class CodeWhispererTelemetryService {
try {
val response = CodeWhispererClientAdaptor.getInstance(project)
.sendUserTriggerDecisionTelemetry(
- sessionContext,
requestContext,
responseContext,
completionType,
@@ -249,7 +246,7 @@ class CodeWhispererTelemetryService {
CodewhispererTelemetry.userTriggerDecision(
project = project,
codewhispererSessionId = responseContext.sessionId,
- codewhispererFirstRequestId = sessionContext.latencyContext.firstRequestId,
+ codewhispererFirstRequestId = requestContext.latencyContext.firstRequestId,
credentialStartUrl = getConnectionStartUrl(requestContext.connection),
codewhispererIsPartialAcceptance = null,
codewhispererPartialAcceptanceCount = null,
@@ -268,7 +265,7 @@ class CodeWhispererTelemetryService {
codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(),
codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(),
codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision,
- codewhispererTimeToFirstRecommendation = sessionContext.latencyContext.paginationFirstCompletionTime,
+ codewhispererTimeToFirstRecommendation = requestContext.latencyContext.paginationFirstCompletionTime,
codewhispererPreviousSuggestionState = previousUserTriggerDecision,
codewhispererSuggestionState = suggestionState,
codewhispererClassifierResult = classifierResult,
@@ -389,66 +386,59 @@ class CodeWhispererTelemetryService {
}
fun sendUserDecisionEventForAll(
+ requestContext: RequestContext,
+ responseContext: ResponseContext,
+ recommendationContext: RecommendationContext,
sessionContext: SessionContext,
hasUserAccepted: Boolean,
popupShownTime: Duration? = null,
) {
- var hasSentRejectedTrigger = false
- CodeWhispererService.getInstance().getAllPaginationSessions().forEach { (jobId, state) ->
- if (state == null) return@forEach
- val details = state.recommendationContext.details
-
- val decisions = details.mapIndexed { index, detail ->
- val suggestionState = recordSuggestionState(detail, hasUserAccepted)
- sendUserDecisionEvent(state.requestContext, state.responseContext, detail, index, suggestionState, details.size)
+ 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)
- suggestionState
- }
- LOG.debug { "jobId: $jobId, userDecisions: [${decisions.joinToString(", ")}]" }
+ decisions.add(suggestionState)
+ }
- with(aggregateUserDecision(decisions, hasSentRejectedTrigger)) {
- // the order of the following matters
- // step 1, send out current decision
+ with(aggregateUserDecision(decisions)) {
+ // the order of the following matters
+ // step 1, send out current decision
+ previousUserTriggerDecisionTimestamp = Instant.now()
- // if we have sent one reject in this display session(which can contain multiple triggers),
- // we will not send rejects for the remaining triggers in this display session.
- if (this == CodewhispererSuggestionState.Reject) {
- hasSentRejectedTrigger = true
- }
- 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 = recommendation.content()
- val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size
- val acceptedCharCount = acceptedContent.length
- sendUserTriggerDecisionEvent(
- sessionContext,
- state.requestContext,
- state.responseContext,
- state.recommendationContext,
- this,
- popupShownTime,
- referenceCount,
- generatedLineCount,
- acceptedCharCount
- )
-
- // 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 referenceCount = if (hasUserAccepted && detailContexts[sessionContext.selectedIndex].recommendation.hasReferences()) 1 else 0
+ val acceptedContent =
+ if (hasUserAccepted) {
+ detailContexts[sessionContext.selectedIndex].recommendation.content()
+ } else {
+ ""
}
- }
+ val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size
+ val acceptedCharCount = acceptedContent.length
+ sendUserTriggerDecisionEvent(
+ requestContext,
+ responseContext,
+ recommendationContext,
+ CodewhispererSuggestionState.from(this.toString()),
+ popupShownTime,
+ referenceCount,
+ generatedLineCount,
+ acceptedCharCount
+ )
+
+ // step 2, put current decision into queue for later reference
+ previousUserTriggerDecisions.add(this)
+ // we need this as well because AutotriggerService will reset the queue periodically
+ CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(this)
}
}
@@ -457,68 +447,96 @@ 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,
- hasRejectedTrigger: Boolean,
- ): CodewhispererSuggestionState {
+ fun aggregateUserDecision(decisions: List): CodewhispererPreviousSuggestionState {
var isEmpty = true
- var isUnseen = true
- var isDiscard = true
for (decision in decisions) {
if (decision == CodewhispererSuggestionState.Accept) {
- return CodewhispererSuggestionState.Accept
+ return CodewhispererPreviousSuggestionState.Accept
} else if (decision == CodewhispererSuggestionState.Reject) {
- return if (!hasRejectedTrigger) {
- CodewhispererSuggestionState.Reject
- } else {
- CodewhispererSuggestionState.Ignore
- }
- } 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) {
+ return CodewhispererPreviousSuggestionState.Reject
+ } else if (decision != CodewhispererSuggestionState.Empty) {
isEmpty = false
}
}
return if (isEmpty) {
- CodewhispererSuggestionState.Empty
- } else if (isDiscard) {
- CodewhispererSuggestionState.Discard
- } else if (isUnseen) {
- CodewhispererSuggestionState.Unseen
+ CodewhispererPreviousSuggestionState.Empty
} else {
- CodewhispererSuggestionState.Ignore
+ CodewhispererPreviousSuggestionState.Discard
}
}
+ fun sendPerceivedLatencyEvent(
+ requestId: String,
+ requestContext: RequestContext,
+ responseContext: ResponseContext,
+ latency: Double,
+ ) {
+ val (project, _, triggerTypeInfo) = requestContext
+ val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType()
+ val startUrl = getConnectionStartUrl(requestContext.connection)
+ CodewhispererTelemetry.perceivedLatency(
+ project = project,
+ codewhispererCompletionType = CodewhispererCompletionType.Line,
+ codewhispererLanguage = codewhispererLanguage,
+ codewhispererRequestId = requestId,
+ codewhispererSessionId = responseContext.sessionId,
+ codewhispererTriggerType = triggerTypeInfo.triggerType,
+ duration = latency,
+ passive = true,
+ credentialStartUrl = startUrl,
+ codewhispererCustomizationArn = requestContext.customizationArn,
+ )
+ }
+
+ fun sendClientComponentLatencyEvent(states: InvocationContext) {
+ val requestContext = states.requestContext
+ val responseContext = states.responseContext
+ val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType()
+ val startUrl = getConnectionStartUrl(requestContext.connection)
+ CodewhispererTelemetry.clientComponentLatency(
+ project = requestContext.project,
+ codewhispererSessionId = responseContext.sessionId,
+ codewhispererRequestId = requestContext.latencyContext.firstRequestId,
+ codewhispererFirstCompletionLatency = requestContext.latencyContext.paginationFirstCompletionTime,
+ codewhispererPreprocessingLatency = requestContext.latencyContext.getCodeWhispererPreprocessingLatency(),
+ codewhispererEndToEndLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency(),
+ codewhispererAllCompletionsLatency = requestContext.latencyContext.getCodeWhispererAllCompletionsLatency(),
+ codewhispererPostprocessingLatency = requestContext.latencyContext.getCodeWhispererPostprocessingLatency(),
+ codewhispererCredentialFetchingLatency = requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency(),
+ codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType,
+ codewhispererCompletionType = CodewhispererCompletionType.Line,
+ codewhispererLanguage = codewhispererLanguage,
+ credentialStartUrl = startUrl,
+ codewhispererCustomizationArn = requestContext.customizationArn,
+ )
+ }
+
fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) {
// Project instance is not needed. We look at these metrics for each clientId.
CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType)
}
fun recordSuggestionState(
- detail: DetailContext,
+ index: Int,
+ selectedIndex: Int,
+ hasSeen: Boolean,
hasUserAccepted: Boolean,
+ isDiscarded: Boolean,
+ isEmpty: Boolean,
): CodewhispererSuggestionState =
- if (detail.recommendation.content().isEmpty()) {
+ if (isEmpty) {
CodewhispererSuggestionState.Empty
- } else if (detail.isDiscarded) {
+ } else if (isDiscarded) {
CodewhispererSuggestionState.Discard
- } else if (!detail.hasSeen) {
+ } else if (!hasSeen) {
CodewhispererSuggestionState.Unseen
} else if (hasUserAccepted) {
- if (detail.isAccepted) {
+ if (selectedIndex == index) {
CodewhispererSuggestionState.Accept
} else {
CodewhispererSuggestionState.Ignore
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt
new file mode 100644
index 0000000000..7ed73527d5
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt
@@ -0,0 +1,520 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry
+
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.RangeMarker
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import kotlinx.coroutines.launch
+import org.apache.commons.collections4.queue.CircularFifoQueue
+import org.jetbrains.annotations.TestOnly
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.amazon.awssdk.services.codewhispererruntime.model.Completion
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
+import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanTelemetryEvent
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutoTriggerService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getGettingStartedTaskType
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.telemetry.CodewhispererCodeScanScope
+import software.aws.toolkits.telemetry.CodewhispererCompletionType
+import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask
+import software.aws.toolkits.telemetry.CodewhispererLanguage
+import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState
+import software.aws.toolkits.telemetry.CodewhispererSuggestionState
+import software.aws.toolkits.telemetry.CodewhispererTelemetry
+import software.aws.toolkits.telemetry.CodewhispererTriggerType
+import software.aws.toolkits.telemetry.Component
+import software.aws.toolkits.telemetry.Result
+import java.time.Duration
+import java.time.Instant
+import java.util.Queue
+
+@Service
+class CodeWhispererTelemetryServiceNew {
+ // store previous 5 userTrigger decisions
+ private val previousUserTriggerDecisions = CircularFifoQueue(5)
+
+ private var previousUserTriggerDecisionTimestamp: Instant? = null
+
+ private val codewhispererTimeSinceLastUserDecision: Double? =
+ previousUserTriggerDecisionTimestamp?.let {
+ Duration.between(it, Instant.now()).toMillis().toDouble()
+ }
+
+ val previousUserTriggerDecision: CodewhispererPreviousSuggestionState?
+ get() = if (previousUserTriggerDecisions.isNotEmpty()) previousUserTriggerDecisions.last() else null
+
+ companion object {
+ fun getInstance(): CodeWhispererTelemetryServiceNew = service()
+ val LOG = getLogger()
+ const val NO_ACCEPTED_INDEX = -1
+ }
+
+ fun sendFailedServiceInvocationEvent(project: Project, exceptionType: String?) {
+ CodewhispererTelemetry.serviceInvocation(
+ project = project,
+ codewhispererCursorOffset = 0,
+ codewhispererLanguage = CodewhispererLanguage.Unknown,
+ codewhispererLastSuggestionIndex = -1,
+ codewhispererLineNumber = 0,
+ codewhispererTriggerType = CodewhispererTriggerType.Unknown,
+ duration = 0.0,
+ reason = exceptionType,
+ success = false,
+ )
+ }
+
+ fun sendServiceInvocationEvent(
+ jobId: Int,
+ requestId: String,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ lastRecommendationIndex: Int,
+ invocationSuccess: Boolean,
+ latency: Double,
+ exceptionType: String?,
+ ) {
+ LOG.debug { "Sending serviceInvocation for $requestId, jobId: $jobId" }
+ val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo
+ val (offset, line) = requestContext.caretPosition
+
+ // since python now only supports UTG but not cross file context
+ val supContext = if (requestContext.fileContextInfo.programmingLanguage.isUTGSupported() &&
+ requestContext.supplementalContext?.isUtg == true
+ ) {
+ requestContext.supplementalContext
+ } else if (requestContext.fileContextInfo.programmingLanguage.isSupplementalContextSupported() &&
+ requestContext.supplementalContext?.isUtg == false
+ ) {
+ requestContext.supplementalContext
+ } else {
+ null
+ }
+
+ val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType()
+ val startUrl = getConnectionStartUrl(requestContext.connection)
+ CodewhispererTelemetry.serviceInvocation(
+ project = requestContext.project,
+ codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType,
+ codewhispererCompletionType = CodewhispererCompletionType.Line,
+ codewhispererCursorOffset = offset.toLong(),
+ codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor),
+ codewhispererLanguage = codewhispererLanguage,
+ codewhispererLastSuggestionIndex = lastRecommendationIndex.toLong(),
+ codewhispererLineNumber = line.toLong(),
+ codewhispererRequestId = requestId,
+ codewhispererSessionId = responseContext.sessionId,
+ codewhispererTriggerType = triggerType,
+ duration = latency,
+ reason = exceptionType,
+ success = invocationSuccess,
+ credentialStartUrl = startUrl,
+ codewhispererImportRecommendationEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled(),
+ codewhispererSupplementalContextTimeout = supContext?.isProcessTimeout,
+ codewhispererSupplementalContextIsUtg = supContext?.isUtg,
+ codewhispererSupplementalContextLatency = supContext?.latency?.toDouble(),
+ codewhispererSupplementalContextLength = supContext?.contentLength?.toLong(),
+ codewhispererCustomizationArn = requestContext.customizationArn,
+ )
+ }
+
+ fun sendUserDecisionEvent(
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ detailContext: DetailContextNew,
+ index: Int,
+ suggestionState: CodewhispererSuggestionState,
+ numOfRecommendations: Int,
+ ) {
+ val requestId = detailContext.requestId
+ val recommendation = detailContext.recommendation
+ val (project, _, triggerTypeInfo) = requestContext
+ val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType()
+ val supplementalContext = requestContext.supplementalContext
+
+ LOG.debug {
+ "Recording user decisions of recommendation. " +
+ "Index: $index, " +
+ "State: $suggestionState, " +
+ "Request ID: $requestId, " +
+ "Recommendation: ${recommendation.content()}"
+ }
+ val startUrl = getConnectionStartUrl(requestContext.connection)
+ val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled()
+ CodewhispererTelemetry.userDecision(
+ project = project,
+ codewhispererCompletionType = detailContext.completionType,
+ codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor),
+ codewhispererLanguage = codewhispererLanguage,
+ codewhispererPaginationProgress = numOfRecommendations.toLong(),
+ codewhispererRequestId = requestId,
+ codewhispererSessionId = responseContext.sessionId,
+ codewhispererSuggestionIndex = index.toLong(),
+ codewhispererSuggestionReferenceCount = recommendation.references().size.toLong(),
+ codewhispererSuggestionReferences = jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList()),
+ codewhispererSuggestionImportCount = if (importEnabled) recommendation.mostRelevantMissingImports().size.toLong() else null,
+ codewhispererSuggestionState = suggestionState,
+ codewhispererTriggerType = triggerTypeInfo.triggerType,
+ credentialStartUrl = startUrl,
+ codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg,
+ codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(),
+ codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout,
+ )
+ }
+
+ fun sendUserTriggerDecisionEvent(
+ sessionContext: SessionContextNew,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ recommendationContext: RecommendationContextNew,
+ suggestionState: CodewhispererSuggestionState,
+ popupShownTime: Duration?,
+ suggestionReferenceCount: Int,
+ generatedLineCount: Int,
+ acceptedCharCount: Int,
+ ) {
+ val project = requestContext.project
+ val totalImportCount = recommendationContext.details.fold(0) { grandTotal, detail ->
+ grandTotal + detail.recommendation.mostRelevantMissingImports().size
+ }
+
+ val automatedTriggerType = requestContext.triggerTypeInfo.automatedTriggerType
+ val triggerChar = if (automatedTriggerType is CodeWhispererAutomatedTriggerType.SpecialChar) {
+ automatedTriggerType.specialChar.toString()
+ } else {
+ null
+ }
+
+ val language = requestContext.fileContextInfo.programmingLanguage
+
+ val classifierResult = requestContext.triggerTypeInfo.automatedTriggerType.calculationResult
+
+ val classifierThreshold = CodeWhispererAutoTriggerService.getThreshold()
+
+ val supplementalContext = requestContext.supplementalContext
+ val completionType = if (recommendationContext.details.isEmpty()) CodewhispererCompletionType.Line else recommendationContext.details[0].completionType
+
+ // only send if it's a pro tier user
+ projectCoroutineScope(project).launch {
+ runIfIdcConnectionOrTelemetryEnabled(project) {
+ try {
+ val response = CodeWhispererClientAdaptor.getInstance(project)
+ .sendUserTriggerDecisionTelemetry(
+ sessionContext,
+ requestContext,
+ responseContext,
+ completionType,
+ suggestionState,
+ suggestionReferenceCount,
+ generatedLineCount,
+ recommendationContext.details.size,
+ acceptedCharCount
+ )
+ LOG.debug {
+ "Successfully sent user trigger decision telemetry. RequestId: ${response.responseMetadata().requestId()}"
+ }
+ } catch (e: Exception) {
+ val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null
+ LOG.debug {
+ "Failed to send user trigger decision telemetry. RequestId: $requestId, ErrorMessage: ${e.message}"
+ }
+ }
+ }
+ }
+
+ CodewhispererTelemetry.userTriggerDecision(
+ project = project,
+ codewhispererSessionId = responseContext.sessionId,
+ codewhispererFirstRequestId = sessionContext.latencyContext.firstRequestId,
+ credentialStartUrl = getConnectionStartUrl(requestContext.connection),
+ codewhispererIsPartialAcceptance = null,
+ codewhispererPartialAcceptanceCount = null,
+ codewhispererCharactersAccepted = acceptedCharCount.toLong(),
+ codewhispererCharactersRecommended = null,
+ codewhispererCompletionType = completionType,
+ codewhispererLanguage = language.toTelemetryType(),
+ codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType,
+ codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType,
+ codewhispererLineNumber = requestContext.caretPosition.line.toLong(),
+ codewhispererCursorOffset = requestContext.caretPosition.offset.toLong(),
+ codewhispererSuggestionCount = recommendationContext.details.size.toLong(),
+ codewhispererSuggestionImportCount = totalImportCount.toLong(),
+ codewhispererTotalShownTime = popupShownTime?.toMillis()?.toDouble(),
+ codewhispererTriggerCharacter = triggerChar,
+ codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(),
+ codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(),
+ codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision,
+ codewhispererTimeToFirstRecommendation = sessionContext.latencyContext.paginationFirstCompletionTime,
+ codewhispererPreviousSuggestionState = previousUserTriggerDecision,
+ codewhispererSuggestionState = suggestionState,
+ codewhispererClassifierResult = classifierResult,
+ codewhispererClassifierThreshold = classifierThreshold,
+ codewhispererCustomizationArn = requestContext.customizationArn,
+ codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg,
+ codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(),
+ codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout,
+ codewhispererSupplementalContextStrategyId = supplementalContext?.strategy.toString(),
+ codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor),
+ codewhispererFeatureEvaluations = CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry()
+ )
+ }
+
+ fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent, project: Project? = null) {
+ val payloadContext = codeScanEvent.codeScanResponseContext.payloadContext
+ val serviceInvocationContext = codeScanEvent.codeScanResponseContext.serviceInvocationContext
+ val codeScanJobId = codeScanEvent.codeScanResponseContext.codeScanJobId
+ val totalIssues = codeScanEvent.codeScanResponseContext.codeScanTotalIssues
+ val issuesWithFixes = codeScanEvent.codeScanResponseContext.codeScanIssuesWithFixes
+ val reason = codeScanEvent.codeScanResponseContext.reason
+ val startUrl = getConnectionStartUrl(codeScanEvent.connection)
+ val codeAnalysisScope = codeScanEvent.codeAnalysisScope
+ val passive = codeAnalysisScope == CodeWhispererConstants.CodeAnalysisScope.FILE
+
+ LOG.debug {
+ "Recording code security scan event. \n" +
+ "Total number of security scan issues found: $totalIssues, \n" +
+ "Number of security scan issues with fixes: $issuesWithFixes, \n" +
+ "Language: ${payloadContext.language}, \n" +
+ "Uncompressed source payload size in bytes: ${payloadContext.srcPayloadSize}, \n" +
+ "Uncompressed build payload size in bytes: ${payloadContext.buildPayloadSize}, \n" +
+ "Compressed source zip file size in bytes: ${payloadContext.srcZipFileSize}, \n" +
+ "Total project size in bytes: ${codeScanEvent.totalProjectSizeInBytes}, \n" +
+ "Total duration of the security scan job in milliseconds: ${codeScanEvent.duration}, \n" +
+ "Context truncation duration in milliseconds: ${payloadContext.totalTimeInMilliseconds}, \n" +
+ "Artifacts upload duration in milliseconds: ${serviceInvocationContext.artifactsUploadDuration}, \n" +
+ "Service invocation duration in milliseconds: ${serviceInvocationContext.serviceInvocationDuration}, \n" +
+ "Total number of lines scanned: ${payloadContext.totalLines}, \n" +
+ "Reason: $reason \n" +
+ "Scope: ${codeAnalysisScope.value} \n" +
+ "Passive: $passive \n"
+ }
+ CodewhispererTelemetry.securityScan(
+ project = project,
+ codewhispererCodeScanLines = payloadContext.totalLines,
+ codewhispererCodeScanJobId = codeScanJobId,
+ codewhispererCodeScanProjectBytes = codeScanEvent.totalProjectSizeInBytes,
+ codewhispererCodeScanSrcPayloadBytes = payloadContext.srcPayloadSize,
+ codewhispererCodeScanBuildPayloadBytes = payloadContext.buildPayloadSize,
+ codewhispererCodeScanSrcZipFileBytes = payloadContext.srcZipFileSize,
+ codewhispererCodeScanTotalIssues = totalIssues.toLong(),
+ codewhispererCodeScanIssuesWithFixes = issuesWithFixes.toLong(),
+ codewhispererLanguage = payloadContext.language,
+ duration = codeScanEvent.duration,
+ contextTruncationDuration = payloadContext.totalTimeInMilliseconds,
+ artifactsUploadDuration = serviceInvocationContext.artifactsUploadDuration,
+ codeScanServiceInvocationsDuration = serviceInvocationContext.serviceInvocationDuration,
+ reason = reason,
+ result = codeScanEvent.result,
+ credentialStartUrl = startUrl,
+ codewhispererCodeScanScope = CodewhispererCodeScanScope.from(codeAnalysisScope.value),
+ passive = passive
+ )
+ }
+
+ fun sendCodeScanIssueHoverEvent(issue: CodeWhispererCodeScanIssue) {
+ CodewhispererTelemetry.codeScanIssueHover(
+ findingId = issue.findingId,
+ detectorId = issue.detectorId,
+ ruleId = issue.ruleId,
+ includesFix = issue.suggestedFixes.isNotEmpty(),
+ credentialStartUrl = getCodeWhispererStartUrl(issue.project)
+ )
+ }
+
+ fun sendCodeScanIssueApplyFixEvent(issue: CodeWhispererCodeScanIssue, result: Result, reason: String? = null) {
+ CodewhispererTelemetry.codeScanIssueApplyFix(
+ findingId = issue.findingId,
+ detectorId = issue.detectorId,
+ ruleId = issue.ruleId,
+ component = Component.Hover,
+ result = result,
+ reason = reason,
+ credentialStartUrl = getCodeWhispererStartUrl(issue.project)
+ )
+ }
+
+ fun enqueueAcceptedSuggestionEntry(
+ requestId: String,
+ requestContext: RequestContextNew,
+ responseContext: ResponseContext,
+ time: Instant,
+ vFile: VirtualFile?,
+ range: RangeMarker,
+ suggestion: String,
+ selectedIndex: Int,
+ completionType: CodewhispererCompletionType,
+ ) {
+ val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage
+ CodeWhispererUserModificationTracker.getInstance(requestContext.project).enqueue(
+ AcceptedSuggestionEntry(
+ time,
+ vFile,
+ range,
+ suggestion,
+ responseContext.sessionId,
+ requestId,
+ selectedIndex,
+ requestContext.triggerTypeInfo.triggerType,
+ completionType,
+ codewhispererLanguage,
+ null,
+ null,
+ requestContext.connection
+ )
+ )
+ }
+
+ fun sendUserDecisionEventForAll(
+ sessionContext: SessionContextNew,
+ hasUserAccepted: Boolean,
+ popupShownTime: Duration? = null,
+ ) {
+ CodeWhispererServiceNew.getInstance().getAllPaginationSessions().forEach { (jobId, state) ->
+ if (state == null) return@forEach
+ val details = state.recommendationContext.details
+
+ val decisions = details.mapIndexed { index, detail ->
+ val suggestionState = recordSuggestionState(detail, hasUserAccepted)
+ sendUserDecisionEvent(state.requestContext, state.responseContext, detail, index, suggestionState, details.size)
+
+ suggestionState
+ }
+ LOG.debug { "jobId: $jobId, userDecisions: [${decisions.joinToString(", ")}]" }
+
+ with(aggregateUserDecision(decisions)) {
+ // the order of the following matters
+ // step 1, send out current decision
+ LOG.debug { "jobId: $jobId, userTriggerDecision: $this" }
+ previousUserTriggerDecisionTimestamp = Instant.now()
+
+ val previews = CodeWhispererServiceNew.getInstance().getAllSuggestionsPreviewInfo()
+ val recommendation =
+ if (hasUserAccepted) {
+ previews[sessionContext.selectedIndex].detail.recommendation
+ } else {
+ Completion.builder().content("").references(emptyList()).build()
+ }
+ val referenceCount = if (hasUserAccepted && recommendation.hasReferences()) 1 else 0
+ val acceptedContent = recommendation.content()
+ val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size
+ val acceptedCharCount = acceptedContent.length
+ sendUserTriggerDecisionEvent(
+ sessionContext,
+ state.requestContext,
+ state.responseContext,
+ state.recommendationContext,
+ this,
+ popupShownTime,
+ referenceCount,
+ generatedLineCount,
+ acceptedCharCount
+ )
+
+ // 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)
+ }
+ }
+ }
+ }
+
+ /**
+ * Aggregate recommendation level user decision to trigger level user decision based on the following rule
+ * - Accept if there is an Accept
+ * - Reject if there is a Reject
+ * - Empty if all decisions are Empty
+ * - Ignore if at least one suggestion is seen and there's an accept for another trigger in the same display session
+ * - Unseen if the whole trigger is not seen (but has valid suggestions)
+ * - Record the accepted suggestion index
+ * - Discard otherwise
+ */
+ fun aggregateUserDecision(decisions: List): CodewhispererSuggestionState {
+ var isEmpty = true
+ var isUnseen = true
+ var isDiscard = true
+
+ for (decision in decisions) {
+ if (decision == CodewhispererSuggestionState.Accept) {
+ return CodewhispererSuggestionState.Accept
+ } else if (decision == CodewhispererSuggestionState.Reject) {
+ return CodewhispererSuggestionState.Reject
+ } else if (decision == CodewhispererSuggestionState.Unseen) {
+ isEmpty = false
+ isDiscard = false
+ } else if (decision == CodewhispererSuggestionState.Ignore) {
+ isUnseen = false
+ isEmpty = false
+ isDiscard = false
+ } else if (decision == CodewhispererSuggestionState.Discard) {
+ isEmpty = false
+ }
+ }
+
+ return if (isEmpty) {
+ CodewhispererSuggestionState.Empty
+ } else if (isDiscard) {
+ CodewhispererSuggestionState.Discard
+ } else if (isUnseen) {
+ CodewhispererSuggestionState.Unseen
+ } else {
+ CodewhispererSuggestionState.Ignore
+ }
+ }
+
+ fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) {
+ // Project instance is not needed. We look at these metrics for each clientId.
+ CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType)
+ }
+
+ fun recordSuggestionState(
+ detail: DetailContextNew,
+ hasUserAccepted: Boolean,
+ ): CodewhispererSuggestionState =
+ if (detail.recommendation.content().isEmpty()) {
+ CodewhispererSuggestionState.Empty
+ } else if (detail.isDiscarded) {
+ CodewhispererSuggestionState.Discard
+ } else if (!detail.hasSeen) {
+ CodewhispererSuggestionState.Unseen
+ } else if (hasUserAccepted) {
+ if (detail.isAccepted) {
+ CodewhispererSuggestionState.Accept
+ } else {
+ CodewhispererSuggestionState.Ignore
+ }
+ } else {
+ CodewhispererSuggestionState.Reject
+ }
+
+ @TestOnly
+ fun previousDecisions(): Queue {
+ assert(ApplicationManager.getApplication().isUnitTestMode)
+ return this.previousUserTriggerDecisions
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/toolwindow/CodeWhispererCodeReferenceActionListener.kt
index 1775631587..c2951db615 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,12 +5,23 @@ 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.InvocationContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener
class CodeWhispererCodeReferenceActionListener : CodeWhispererUserActionListener {
- override fun afterAccept(states: InvocationContext, previews: List, sessionContext: SessionContext, rangeMarker: RangeMarker) {
+ 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)
+ }
+}
+
+class CodeWhispererCodeReferenceActionListenerNew : CodeWhispererUserActionListener {
+ override fun afterAccept(states: InvocationContextNew, previews: List, sessionContext: SessionContextNew, 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 8adafe97b9..60ddf3987d 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.InvocationContextNew
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
@@ -117,7 +118,7 @@ class CodeWhispererCodeReferenceManager(private val project: Project) {
insertCodeReference(detail.content(), reformattedDetail.references(), editor, caretPosition, detail)
}
- fun insertCodeReference(states: InvocationContext, previews: List, selectedIndex: Int) {
+ fun insertCodeReference(states: InvocationContextNew, previews: List, selectedIndex: Int) {
val detail = previews[selectedIndex].detail
insertCodeReference(
detail.recommendation.content(),
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 cee402f7a0..ad4968d2eb 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
@@ -7,6 +7,12 @@ import com.intellij.ide.DataManager
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
@@ -16,9 +22,6 @@ 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
@@ -122,9 +125,9 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() {
@Test
fun `test CodeWhisperer keyboard shortcuts should be prioritized to be executed`() {
- testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_ACCEPT)
- testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_PREV)
- testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_NEXT)
+ testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_TAB)
+ testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_RIGHT)
+ testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_LEFT)
}
private fun testCodeWhispererKeyboardShortcutShouldBePrioritized(actionId: String) {
@@ -157,8 +160,8 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() {
// move the cursor to the correct trigger point (...void main)
projectRule.fixture.editor.caretModel.moveToOffset(47)
}
- withCodeWhispererServiceInvokedAndWait { session ->
- val recommendation = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].detail.reformatted.content()
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val recommendation = states.recommendationContext.details[0].reformatted.content()
val editor = projectRule.fixture.editor
val expectedContext = buildContextWithRecommendation(recommendation + remaining)
val startOffset = editor.caretModel.offset
@@ -173,9 +176,8 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() {
private fun acceptHelper(useKeyboard: Boolean) {
if (useKeyboard) {
- ActionManager.getInstance().getAction(ACTION_KEY_ACCEPT).actionPerformed(
- AnActionEvent.createFromDataContext("test", null) { projectRule.project }
- )
+ EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_TAB)
+ .execute(projectRule.fixture.editor, 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 64f37b633d..854d74469a 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
@@ -83,11 +83,10 @@ 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.ResponseContext
-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
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule
@@ -239,10 +238,10 @@ class CodeWhispererClientAdaptorTest {
@Test
fun sendUserTriggerDecisionTelemetry() {
- val mockModelConfigurator = mock {
+ val mockModelConfiguraotr = mock {
on { activeCustomization(any()) } doReturn CodeWhispererCustomization("fake-arn", "fake-name")
}
- ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfigurator, disposableRule.disposable)
+ ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfiguraotr, disposableRule.disposable)
val file = projectRule.fixture.addFileToProject("main.java", "public class Main {}")
runInEdtAndWait {
@@ -253,14 +252,10 @@ 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,
@@ -268,7 +263,7 @@ class CodeWhispererClientAdaptorTest {
3,
1,
2,
- 10,
+ 10
)
argumentCaptor().apply {
@@ -381,7 +376,6 @@ 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(),
@@ -389,7 +383,7 @@ class CodeWhispererClientAdaptorTest {
0,
1,
2,
- 10,
+ 10
)
}
}
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 371d9538e2..a82f8700eb 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,14 +53,13 @@ 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.RequestContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager.Companion.CODEWHISPERER_USER_ACTION_PERFORMED
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeCoverageTokens
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
@@ -176,11 +175,12 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
}
},
null,
+ mock(),
aString()
)
val responseContext = ResponseContext("sessionId")
val recommendationContext = RecommendationContext(
- mutableListOf(
+ listOf(
DetailContext(
"requestId",
pythonResponse.completions()[0],
@@ -193,11 +193,10 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
),
"x, y",
"x, y",
- mock(),
- 0
+ mock()
)
- invocationContext = InvocationContext(requestContext, responseContext, recommendationContext)
- sessionContext = SessionContext(project, fixture.editor, latencyContext = LatencyContext())
+ invocationContext = InvocationContext(requestContext, responseContext, recommendationContext, mock())
+ sessionContext = SessionContext()
// it is needed because referenceManager is listening to CODEWHISPERER_USER_ACTION_PERFORMED topic
project.replaceService(CodeWhispererCodeReferenceManager::class.java, mock(), disposableRule.disposable)
@@ -340,7 +339,6 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_USER_ACTION_PERFORMED).afterAccept(
invocationContext,
mock(),
- SessionContext(project, fixture.editor, latencyContext = LatencyContext()),
rangeMarkerMock
)
@@ -424,7 +422,6 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
metricCaptor.allValues,
CODE_PERCENTAGE,
1,
- CWSPR_PERCENTAGE to "3",
CWSPR_ACCEPTED_TOKENS to "2",
CWSPR_RAW_ACCEPTED_TOKENS to "3",
CWSPR_TOTAL_TOKENS to "100",
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 ab95d5bff4..1e3d42ffff 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,12 +3,11 @@
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
@@ -35,10 +34,10 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() {
}
private fun testNavigation(isReverse: Boolean, useKeyboard: Boolean = false) {
- withCodeWhispererServiceInvokedAndWait { session ->
+ withCodeWhispererServiceInvokedAndWait {
val indexChange = if (isReverse) -1 else 1
- assertThat(session.selectedIndex).isEqualTo(0)
+ assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(0)
val expectedCount = pythonResponse.completions().size
var expectedSelectedIndex: Int
@@ -59,7 +58,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() {
}
}
- assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex)
+ assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex)
assertThat(oppositeButton.isEnabled).isEqualTo(false)
repeat(expectedCount - 1) {
@@ -67,7 +66,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() {
navigateHelper(isReverse, useKeyboard)
assertThat(oppositeButton.isEnabled).isEqualTo(true)
expectedSelectedIndex = (expectedSelectedIndex + indexChange) % expectedCount
- assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex)
+ assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex)
checkRecommendationInfoLabelText(expectedSelectedIndex + 1, expectedCount)
}
assertThat(navigationButton.isEnabled).isEqualTo(false)
@@ -76,13 +75,13 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() {
private fun navigateHelper(isReverse: Boolean, useKeyboard: Boolean) {
if (useKeyboard) {
- val actionHandler = ActionManager.getInstance()
+ val actionHandler = EditorActionManager.getInstance()
if (isReverse) {
- val leftArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_PREV)
- leftArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null) { projectRule.project })
+ val leftArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT)
+ leftArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT)
} else {
- val rightArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_NEXT)
- rightArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null) { projectRule.project })
+ val rightArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT)
+ rightArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT)
}
} else {
if (isReverse) {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt
index 221bf896fd..c80416c986 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt
@@ -19,8 +19,8 @@ import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
import software.aws.toolkits.core.utils.test.aString
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.RequestContext
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule
class CodeWhispererRecommendationManagerTest {
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 0a050493a5..f186740f1e 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 { session ->
- codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail }.forEach {
+ withCodeWhispererServiceInvokedAndWait { states ->
+ states.recommendationContext.details.forEach {
assertThat(it.recommendation.references().isEmpty()).isEqualTo(invalid)
}
popupManagerSpy.popupComponents.acceptButton.doClick()
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt
index 5d83170927..e4202cba6b 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 {
- val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val firstRecommendation = states.recommendationContext.details[0]
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 { _ ->
- val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val firstRecommendation = states.recommendationContext.details[0]
assertThat(firstRecommendation.isDiscarded).isEqualTo(true)
- val details = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail }
- details.forEach { detail ->
- assertThat(detail.reformatted.references().isEmpty())
+ val details = states.recommendationContext.details
+ details.forEach {
+ assertThat(it.reformatted.references().isEmpty())
}
}
}
@@ -47,9 +47,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() {
val lastNewLineIndex = firstRecommendationContent.lastIndexOf('\n')
val rightContext = firstRecommendationContent.substring(lastNewLineIndex)
setFileContext(pythonFileName, pythonTestLeftContext, rightContext)
- withCodeWhispererServiceInvokedAndWait {
- val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail
- assertThat(firstDetail.isDiscarded).isEqualTo(false)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val firstRecommendation = states.recommendationContext.details[0]
+ assertThat(firstRecommendation.isDiscarded).isEqualTo(false)
+ val firstDetail = states.recommendationContext.details[0]
val span = firstDetail.reformatted.references()[0].recommendationContentSpan()
assertThat(span.start()).isEqualTo(0)
assertThat(span.end()).isEqualTo(lastNewLineIndex)
@@ -83,8 +84,9 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() {
}
withCodeWhispererServiceInvokedAndWait { states ->
- val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail
- assertThat(firstDetail.isDiscarded).isEqualTo(false)
+ val firstRecommendation = states.recommendationContext.details[0]
+ assertThat(firstRecommendation.isDiscarded).isEqualTo(false)
+ val firstDetail = states.recommendationContext.details[0]
val span = firstDetail.reformatted.references()[0].recommendationContentSpan()
assertThat(span.start()).isEqualTo(0)
assertThat(span.end()).isEqualTo(lastNewLineIndex)
@@ -99,10 +101,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() {
val remaining = firstRecommendation.substring(0, remainingLength)
val rightContext = pythonResponse.completions()[0].content().substring(remainingLength)
setFileContext(pythonFileName, pythonTestLeftContext, rightContext)
- withCodeWhispererServiceInvokedAndWait { session ->
- assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining)
popupManagerSpy.popupComponents.acceptButton.doClick()
- assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext)
+ assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext)
}
}
@@ -113,8 +115,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() {
val remainingLength = Random.nextInt(newLineIndex, firstRecommendation.length)
val rightContext = pythonResponse.completions()[0].content().substring(remainingLength)
setFileContext(pythonFileName, pythonTestLeftContext, rightContext)
- withCodeWhispererServiceInvokedAndWait {
- assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.recommendation.content()).isEqualTo(firstRecommendation)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation)
}
}
@@ -126,10 +128,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() {
val remaining = firstRecommendation.substring(0, remainingLength)
val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) + "test"
setFileContext(pythonFileName, pythonTestLeftContext, rightContext)
- withCodeWhispererServiceInvokedAndWait { session ->
- assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining)
popupManagerSpy.popupComponents.acceptButton.doClick()
- assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext)
+ assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext)
}
}
@@ -140,8 +142,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 {
- assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.recommendation.content()).isEqualTo(firstRecommendation)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation)
}
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt
index 3c2d8e1c0f..1d5646d386 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,7 +12,6 @@ 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
@@ -20,9 +19,9 @@ import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
-import org.mockito.kotlin.timeout
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -37,16 +36,17 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.customization.Code
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition
import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk
import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.RequestContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider
import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule
@@ -95,9 +95,31 @@ class CodeWhispererServiceTest {
projectRule.project.replaceService(AwsConnectionManager::class.java, mock(), disposableRule.disposable)
}
- @After
- fun tearDown() {
- sut.disposeDisplaySession(false)
+ @Test
+ fun `getRequestContext should use correct fileContext and timeout to fetch supplementalContext`() = runTest {
+ val fileContextProvider = FileContextProvider.getInstance(projectRule.project)
+ val fileContextProviderSpy = spy(fileContextProvider)
+ projectRule.project.replaceService(FileContextProvider::class.java, fileContextProviderSpy, disposableRule.disposable)
+
+ val requestContext = sut.getRequestContext(
+ TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, CodeWhispererAutomatedTriggerType.Enter()),
+ editor = projectRule.fixture.editor,
+ project = projectRule.project,
+ file,
+ LatencyContext()
+ )
+
+ requestContext.awaitSupplementalContext()
+ val fileContextCaptor = argumentCaptor()
+ verify(fileContextProviderSpy, times(1)).extractSupplementalFileContext(eq(file), fileContextCaptor.capture(), eq(100))
+ assertThat(fileContextCaptor.firstValue).isEqualTo(
+ FileContextInfo(
+ CaretContext(leftFileContext = "", rightFileContext = "public class Main {}", leftContextOnCurrentLine = ""),
+ "main.java",
+ CodeWhispererJava.INSTANCE,
+ "main.java"
+ )
+ )
}
@Test
@@ -131,7 +153,8 @@ class CodeWhispererServiceTest {
TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()),
projectRule.fixture.editor,
projectRule.project,
- file
+ file,
+ LatencyContext()
)
runTest {
@@ -155,7 +178,8 @@ class CodeWhispererServiceTest {
TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()),
projectRule.fixture.editor,
projectRule.project,
- file
+ file,
+ LatencyContext()
)
assertThat(actual.supplementalContext).isNotNull
@@ -187,13 +211,14 @@ class CodeWhispererServiceTest {
fileContextInfo = mockFileContext,
supplementalContextDeferred = async { mockSupContext },
connection = ToolkitConnectionManager.getInstance(projectRule.project).activeConnection(),
+ latencyContext = LatencyContext(),
customizationArn = "fake-arn"
)
)
- sut.invokeCodeWhispererInBackground(mockRequestContext, 0, LatencyContext())
+ sut.invokeCodeWhispererInBackground(mockRequestContext).join()
- verify(mockRequestContext, timeout(5000).atLeastOnce()).awaitSupplementalContext()
+ verify(mockRequestContext, times(1)).awaitSupplementalContext()
verify(clientFacade).generateCompletionsPaginator(any())
argumentCaptor {
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 e9e7a30e18..3a640c15bc 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,6 +4,7 @@
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
@@ -17,12 +18,14 @@ 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.status.CodeWhispererStatusBarWidgetFactory
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceToolWindowFactory
import software.aws.toolkits.jetbrains.settings.CodeWhispererConfiguration
@@ -31,11 +34,14 @@ 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) {
@@ -77,7 +83,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
whenever(stateManager.checkActiveCodeWhispererConnectionType(projectRule.project)).thenReturn(CodeWhispererLoginType.Logout)
assertThat(isCodeWhispererEnabled(projectRule.project)).isFalse
invokeCodeWhispererService()
- verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any())
+ verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any())
}
@Test
@@ -86,7 +92,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
assertThat(stateManager.isAutoEnabled()).isFalse
runInEdtAndWait {
projectRule.fixture.type(':')
- verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any())
+ verify(codewhispererServiceSpy, 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 93c9365400..0397576e8b 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,16 +12,13 @@ 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 { session ->
- val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId
- val actualRequestContext = codewhispererService.ongoingRequestsContext[selectedJobId]
- assertNotNull(actualRequestContext)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val actualRequestContext = states.requestContext
val editor = projectRule.fixture.editor
val (actualProject, actualEditor, actualTriggerTypeInfo, actualCaretPosition, actualFileContextInfo) = actualRequestContext
val (actualCaretContext, actualFilename, actualProgrammingLanguage) = actualFileContextInfo
@@ -49,10 +46,8 @@ class CodeWhispererStateTest : CodeWhispererTestBase() {
@Test
fun `test CodeWhisperer invocation sets response metadata correctly`() {
- withCodeWhispererServiceInvokedAndWait { session ->
- val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId
- val actualResponseContext = codewhispererService.getAllPaginationSessions()[selectedJobId]?.responseContext
- assertNotNull(actualResponseContext)
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val actualResponseContext = states.responseContext
assertThat(listOf(actualResponseContext.sessionId)).isEqualTo(
pythonResponse.sdkHttpResponse().headers()[CodeWhispererService.KET_SESSION_ID]
)
@@ -61,9 +56,7 @@ class CodeWhispererStateTest : CodeWhispererTestBase() {
@Test
fun `test CodeWhisperer invocation sets recommendation metadata correctly`() {
- withCodeWhispererServiceInvokedAndWait {
- val states = codewhispererService.getAllPaginationSessions()[0]
- assertNotNull(states)
+ withCodeWhispererServiceInvokedAndWait { states ->
val actualRecommendationContext = states.recommendationContext
val (actualDetailContexts, actualUserInput) = actualRecommendationContext
@@ -81,13 +74,15 @@ class CodeWhispererStateTest : CodeWhispererTestBase() {
@Test
fun `test CodeWhisperer invocation sets initial typeahead and selected index correctly`() {
- withCodeWhispererServiceInvokedAndWait { session ->
- val actualSelectedIndex = session.selectedIndex
- val preview = codewhispererService.getAllSuggestionsPreviewInfo()[actualSelectedIndex]
- val actualTypeahead = preview.typeahead
+ withCodeWhispererServiceInvokedAndWait {
+ val sessionContext = popupManagerSpy.sessionContext
+ val actualSelectedIndex = sessionContext.selectedIndex
+ val actualTypeahead = sessionContext.typeahead
+ val actualTypeaheadOriginal = sessionContext.typeaheadOriginal
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 0de2d4b4c8..6314087592 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,10 +37,8 @@ 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
@@ -88,7 +86,7 @@ class CodeWhispererTelemetryServiceTest {
mockClient = spy(CodeWhispererClientAdaptor.getInstance(projectRule.project))
mockClient.stub {
onGeneric {
- sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any(), any(), any())
+ sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any(), any())
}.doAnswer {
mock()
}
@@ -99,20 +97,25 @@ 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 = aRecommendationContext(expectedStates)
+ val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(expectedStates)
val hasUserAccepted = expectedStates.any { it == CodewhispererSuggestionState.Accept }
val details = recommendationContext.details
val actualStates = mutableListOf()
details.forEachIndexed { index, detail ->
- val suggestionState = sut.recordSuggestionState(detail, hasUserAccepted)
+ val suggestionState = sut.recordSuggestionState(
+ index,
+ sessionContext.selectedIndex,
+ sessionContext.seen.contains(index),
+ hasUserAccepted,
+ detail.isDiscarded,
+ detail.recommendation.content().isEmpty()
+ )
actualStates.add(suggestionState)
}
@@ -153,8 +156,8 @@ class CodeWhispererTelemetryServiceTest {
@Test
fun `test aggregateUserDecision`() {
- fun assertAggregateUserDecision(decisions: List, expected: CodewhispererSuggestionState) {
- val actual = sut.aggregateUserDecision(decisions, false)
+ fun assertAggregateUserDecision(decisions: List, expected: CodewhispererPreviousSuggestionState) {
+ val actual = sut.aggregateUserDecision(decisions)
assertThat(actual).isEqualTo(expected)
}
@@ -166,7 +169,7 @@ class CodeWhispererTelemetryServiceTest {
CodewhispererSuggestionState.Unseen,
CodewhispererSuggestionState.Unseen
),
- CodewhispererSuggestionState.Accept
+ CodewhispererPreviousSuggestionState.Accept
)
assertAggregateUserDecision(
@@ -177,7 +180,7 @@ class CodeWhispererTelemetryServiceTest {
CodewhispererSuggestionState.Empty,
CodewhispererSuggestionState.Ignore
),
- CodewhispererSuggestionState.Reject
+ CodewhispererPreviousSuggestionState.Reject
)
assertAggregateUserDecision(
@@ -188,7 +191,7 @@ class CodeWhispererTelemetryServiceTest {
CodewhispererSuggestionState.Discard,
CodewhispererSuggestionState.Empty
),
- CodewhispererSuggestionState.Discard
+ CodewhispererPreviousSuggestionState.Discard
)
assertAggregateUserDecision(
@@ -198,26 +201,7 @@ class CodeWhispererTelemetryServiceTest {
CodewhispererSuggestionState.Empty,
CodewhispererSuggestionState.Empty
),
- CodewhispererSuggestionState.Empty
- )
- }
-
- @Test
- fun `test aggregateUserDecision when there was a previous reject`() {
- fun assertAggregateUserDecision(decisions: List, expected: CodewhispererSuggestionState) {
- val actual = sut.aggregateUserDecision(decisions, true)
- assertThat(actual).isEqualTo(expected)
- }
-
- assertAggregateUserDecision(
- listOf(
- CodewhispererSuggestionState.Discard,
- CodewhispererSuggestionState.Discard,
- CodewhispererSuggestionState.Reject,
- CodewhispererSuggestionState.Empty,
- CodewhispererSuggestionState.Ignore
- ),
- CodewhispererSuggestionState.Ignore
+ CodewhispererPreviousSuggestionState.Empty
)
}
@@ -235,7 +219,6 @@ class CodeWhispererTelemetryServiceTest {
)
val supplementalContextInfo = aSupplementalContextInfo()
- val sessionContext = aSessionContext(projectRule.project)
val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also {
runTest { it.awaitSupplementalContext() }
}
@@ -252,7 +235,6 @@ class CodeWhispererTelemetryServiceTest {
}
sut.sendUserTriggerDecisionEvent(
- sessionContext,
requestContext,
responseContext,
recommendationContext,
@@ -270,7 +252,7 @@ class CodeWhispererTelemetryServiceTest {
"codewhisperer_userTriggerDecision",
1,
"codewhispererSessionId" to responseContext.sessionId,
- "codewhispererFirstRequestId" to sessionContext.latencyContext.firstRequestId,
+ "codewhispererFirstRequestId" to requestContext.latencyContext.firstRequestId,
"codewhispererCompletionType" to recommendationContext.details[0].completionType,
"codewhispererLanguage" to requestContext.fileContextInfo.programmingLanguage.toTelemetryType(),
"codewhispererTriggerType" to requestContext.triggerTypeInfo.triggerType,
@@ -299,29 +281,21 @@ class CodeWhispererTelemetryServiceTest {
@Test
fun `sendUserDecisionEventForAll will send userDecision event for all suggestions`() {
- doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any(), any())
+ doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any())
val eventCount = mutableMapOf()
var totalEventCount = 0
+ val requestContext = aRequestContext(projectRule.project)
+ val responseContext = aResponseContext()
fun assertUserDecision(decisions: List) {
decisions.forEach { eventCount[it] = 1 + (eventCount[it] ?: 0) }
totalEventCount += decisions.size
- CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext(
- aRequestContext(projectRule.project),
- aResponseContext(),
- aRecommendationContext(decisions)
- )
+ val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(decisions)
val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept }
val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30))
- val sessionContext = aSessionContext(projectRule.project)
- sessionContext.selectedIndex = 0
- sut.sendUserDecisionEventForAll(
- sessionContext,
- hasUserAccept,
- popupShownDuration
- )
+ sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration)
argumentCaptor().apply {
verify(batcher, atLeastOnce()).enqueue(capture())
@@ -374,21 +348,12 @@ 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))
- CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext(
- requestContext,
- aResponseContext(),
- aRecommendationContext(decisions)
- )
- val sessionContext = aSessionContext(projectRule.project)
- sessionContext.selectedIndex = 0
- sut.sendUserDecisionEventForAll(
- sessionContext,
- hasUserAccept,
- popupShownDuration
- )
+ sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration)
argumentCaptor().apply {
verify(batcher, atLeastOnce()).enqueue(capture())
@@ -474,7 +439,6 @@ class CodeWhispererTelemetryServiceTest {
)
AwsSettings.getInstance().isTelemetryEnabled = isTelemetryEnabled
- val expectedSessionContext = aSessionContext(projectRule.project)
val expectedRequestContext = aRequestContext(projectRule.project)
val expectedResponseContext = aResponseContext()
val expectedRecommendationContext = aRecommendationContext()
@@ -485,7 +449,6 @@ class CodeWhispererTelemetryServiceTest {
val expectedCharCount = 100
val expectedCompletionType = expectedRecommendationContext.details[0].completionType
sut.sendUserTriggerDecisionEvent(
- expectedSessionContext,
expectedRequestContext,
expectedResponseContext,
expectedRecommendationContext,
@@ -493,12 +456,11 @@ class CodeWhispererTelemetryServiceTest {
expectedDuration,
expectedSuggestionReferenceCount,
expectedGeneratedLineCount,
- expectedCharCount,
+ expectedCharCount
)
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 4ba1665cd3..6bd9d00629 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,6 +7,7 @@ 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
@@ -57,8 +58,6 @@ 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
@@ -74,7 +73,6 @@ 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"
@@ -107,10 +105,12 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
@Test
fun `test pre-setup failure will send service invocation event with failed status`() {
- codewhispererService.stub {
- onGeneric { getRequestContext(any(), any(), any(), any()) }
+ val codewhispererServiceSpy = spy(codewhispererService) {
+ onGeneric { getRequestContext(any(), any(), any(), any(), any()) }
.doAnswer { throw Exception() }
}
+ ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable)
+
invokeCodeWhispererService()
argumentCaptor().apply {
@@ -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 ->
- codewhispererService.disposeDisplaySession(false)
+ popupManagerSpy.cancelPopup(states.popup)
val count = pythonResponse.completions().size
argumentCaptor().apply {
@@ -373,45 +373,68 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
@Test
fun `test invoking CodeWhisperer will send service invocation event with sessionId and requestId from response`() {
- withCodeWhispererServiceInvokedAndWait { session ->
+ withCodeWhispererServiceInvokedAndWait { states ->
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 detail.requestId,
+ "codewhispererRequestId" to states.recommendationContext.details[0].requestId,
)
}
}
@Test
fun `test userDecision events will record sessionId and requestId from response`() {
- 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())
+ val statesCaptor = argumentCaptor()
+ withCodeWhispererServiceInvokedAndWait {}
+ verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(statesCaptor.capture(), any(), any(), any(), any())
+ val states = statesCaptor.lastValue
val metricCaptor = argumentCaptor()
verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())
- assertNotNull(states)
- assertNotNull(previews)
assertEventsContainsFieldsAndCount(
metricCaptor.allValues,
userDecision,
- previews?.size ?: 0,
- "codewhispererSessionId" to states?.responseContext?.sessionId,
- "codewhispererRequestId" to previews?.get(0)?.detail?.requestId,
+ states.recommendationContext.details.size,
+ "codewhispererSessionId" to states.responseContext.sessionId,
+ "codewhispererRequestId" to states.recommendationContext.details[0].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 {
@@ -649,7 +672,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
}
invokeCodeWhispererService()
- verify(popupManagerSpy, never()).showPopup(any(), any())
+ verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any())
runInEdtAndWait {
val metricCaptor = argumentCaptor()
verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())
@@ -670,9 +693,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
} doReturnConsecutively(listOf(pythonResponseWithNonEmptyToken, emptyListResponse))
}
- withCodeWhispererServiceInvokedAndWait {
- popupManagerSpy.popupComponents.acceptButton.doClick()
- }
+ withCodeWhispererServiceInvokedAndWait { }
runInEdtAndWait {
val metricCaptor = argumentCaptor()
@@ -710,11 +731,11 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
}
@Test
- fun `test toggle autoSuggestion will emit autoSuggestionActivation telemetry (popup)`() {
+ fun `test toggle autoSugestion will emit autoSuggestionActivation telemetry (popup)`() {
val metricCaptor = argumentCaptor()
doNothing().`when`(batcher).enqueue(metricCaptor.capture())
- Pause().actionPerformed(TestActionEvent.createTestEvent { projectRule.project })
+ Pause().actionPerformed(TestActionEvent { projectRule.project })
assertEventsContainsFieldsAndCount(
metricCaptor.allValues,
awsModifySetting,
@@ -723,7 +744,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
"settingState" to CodeWhispererConstants.AutoSuggestion.DEACTIVATED
)
- Resume().actionPerformed(TestActionEvent.createTestEvent { projectRule.project })
+ Resume().actionPerformed(TestActionEvent { projectRule.project })
assertEventsContainsFieldsAndCount(
metricCaptor.allValues,
awsModifySetting,
@@ -751,12 +772,13 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
val numOfEmptyRecommendations = response.completions().filter { it.content().isEmpty() }.size
if (numOfEmptyRecommendations == response.completions().size) {
- verify(popupManagerSpy, never()).showPopup(any(), any())
+ verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any())
} else {
+ val popupCaptor = argumentCaptor()
verify(popupManagerSpy, timeout(5000))
- .showPopup(any(), any())
+ .showPopup(any(), any(), popupCaptor.capture(), any(), any())
runInEdtAndWait {
- codewhispererService.disposeDisplaySession(true)
+ popupManagerSpy.closePopup(popupCaptor.lastValue)
}
}
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 93f65236fa..c6687d501b 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,6 +19,7 @@ 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
@@ -40,7 +41,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.SessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
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
@@ -96,11 +97,9 @@ open class CodeWhispererTestBase {
}
}
- codewhispererService = spy(CodeWhispererService.getInstance())
- ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererService, disposableRule.disposable)
-
popupManagerSpy = spy(CodeWhispererPopupManager.getInstance())
- codewhispererService.disposeDisplaySession(false)
+ popupManagerSpy.reset()
+ doNothing().`when`(popupManagerSpy).showPopup(any(), any(), any(), any(), any())
ApplicationManager.getApplication().replaceService(CodeWhispererPopupManager::class.java, popupManagerSpy, disposableRule.disposable)
invocationStatusSpy = spy(CodeWhispererInvocationStatus.getInstance())
@@ -115,6 +114,7 @@ open class CodeWhispererTestBase {
stateManager = spy(CodeWhispererExplorerActionManager.getInstance())
recommendationManager = CodeWhispererRecommendationManager.getInstance()
+ codewhispererService = CodeWhispererService.getInstance()
editorManager = CodeWhispererEditorManager.getInstance()
settingsManager = CodeWhispererSettings.getInstance()
@@ -153,22 +153,24 @@ open class CodeWhispererTestBase {
open fun tearDown() {
stateManager.loadState(originalExplorerActionState)
settingsManager.loadState(originalSettings)
- codewhispererService.disposeDisplaySession(true)
+ popupManagerSpy.reset()
+ runInEdtAndWait {
+ popupManagerSpy.closePopup()
+ }
}
- fun withCodeWhispererServiceInvokedAndWait(runnable: (SessionContext) -> Unit) {
- val sessionCaptor = argumentCaptor()
+ fun withCodeWhispererServiceInvokedAndWait(runnable: (InvocationContext) -> Unit) {
+ val statesCaptor = argumentCaptor()
invokeCodeWhispererService()
verify(popupManagerSpy, timeout(5000).atLeastOnce())
- .showPopup(sessionCaptor.capture(), any())
- CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true)
- val session = sessionCaptor.lastValue
+ .showPopup(statesCaptor.capture(), any(), any(), any(), any())
+ val states = statesCaptor.lastValue
runInEdtAndWait {
try {
- runnable(session)
+ runnable(states)
} finally {
- codewhispererService.disposeDisplaySession(true)
+ CodeWhispererPopupManager.getInstance().closePopup(states.popup)
}
}
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 50cc14009f..87aa376692 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,14 +34,58 @@ 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 { session ->
- var preview = codewhispererService.getAllSuggestionsPreviewInfo()[0]
- val recommendation = preview.detail.reformatted.content()
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val recommendation = states.recommendationContext.details[0].reformatted.content()
val editor = projectRule.fixture.editor
val startOffset = editor.caretModel.offset
recommendation.forEachIndexed { index, char ->
@@ -49,8 +93,7 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() {
projectRule.fixture.type(char)
val caretOffset = editor.caretModel.offset
val typeahead = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString()
- preview = codewhispererService.getAllSuggestionsPreviewInfo()[0]
- assertThat(preview.typeahead).isEqualTo(typeahead)
+ assertThat(popupManagerSpy.sessionContext.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 c08238b7a1..d46a603e61 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,12 +14,14 @@ 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
@@ -93,7 +95,7 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() {
}
withCodeWhispererServiceInvokedAndWait {
projectRule.fixture.performEditorAction(actionId)
- verify(codewhispererService, timeout(5000).atLeastOnce()).disposeDisplaySession(false)
+ verify(popupManagerSpy, timeout(5000)).cancelPopup(any())
}
}
@@ -118,9 +120,11 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() {
projectRule.fixture.type('\n')
val expectedFileContext = "$testLeftContext\n \n $testRightContext"
assertThat(projectRule.fixture.editor.document.text).isEqualTo(expectedFileContext)
- verify(popupManagerSpy, timeout(5000)).showPopup(any(), any())
+ val popupCaptor = argumentCaptor()
+ verify(popupManagerSpy, timeout(5000))
+ .showPopup(any(), any(), popupCaptor.capture(), any(), any())
runInEdtAndWait {
- codewhispererService.disposeDisplaySession(true)
+ popupManagerSpy.closePopup(popupCaptor.lastValue)
}
}
@@ -134,10 +138,10 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() {
on { this.newRectangle } doReturn newRect
}
withCodeWhispererServiceInvokedAndWait { states ->
- CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true)
+ CodeWhispererInvocationStatus.getInstance().setPopupActive(true)
val listener = CodeWhispererScrollListener(states)
listener.visibleAreaChanged(event)
- verify(popupManagerSpy, times(2)).showPopup(any(), any())
+ verify(popupManagerSpy, times(2)).showPopup(any(), any(), any(), any(), any())
}
}
@@ -159,12 +163,15 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() {
setFileContext(pythonFileName, "def", rightContext)
projectRule.fixture.type('{')
if (shouldtrigger) {
- verify(popupManagerSpy, timeout(5000).atLeastOnce()).showPopup(any(), any())
+ val popupCaptor = argumentCaptor()
+ verify(popupManagerSpy, timeout(5000).atLeastOnce())
+ .showPopup(any(), any(), popupCaptor.capture(), any(), any())
runInEdtAndWait {
- codewhispererService.disposeDisplaySession(true)
+ popupManagerSpy.closePopup(popupCaptor.lastValue)
}
} else {
- verify(popupManagerSpy, times(0)).showPopup(any(), any())
+ verify(popupManagerSpy, times(0))
+ .showPopup(any(), any(), any(), any(), any())
}
}
@@ -172,9 +179,11 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() {
CodeWhispererExplorerActionManager.getInstance().setAutoEnabled(true)
setFileContext(pythonFileName, prompt, "")
projectRule.fixture.type('\n')
- verify(popupManagerSpy, timeout(5000).atLeast(times)).showPopup(any(), any())
+ val popupCaptor = argumentCaptor()
+ verify(popupManagerSpy, timeout(5000).atLeast(times))
+ .showPopup(any(), any(), popupCaptor.capture(), any(), any())
runInEdtAndWait {
- codewhispererService.disposeDisplaySession(true)
+ popupManagerSpy.closePopup(popupCaptor.lastValue)
}
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt
index f534eeb37b..8c4c562481 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,16 +3,21 @@
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() {
@@ -20,8 +25,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
fun `test no user input should show all recommendations`() {
addUserInputAfterInvocation("")
- withCodeWhispererServiceInvokedAndWait { session ->
- val actualRecommendations = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail.recommendation.content() }
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val actualRecommendations = states.recommendationContext.details.map {
+ it.recommendation.content()
+ }
assertThat(actualRecommendations).isEqualTo(pythonResponse.completions().map { it.content() })
}
}
@@ -33,11 +40,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
val expectedRecommendations = pythonResponse.completions().map { it.content() }
- withCodeWhispererServiceInvokedAndWait { session ->
- val previews = codewhispererService.getAllSuggestionsPreviewInfo()
- val actualRecommendations = previews.map { it.detail.recommendation.content() }
+ withCodeWhispererServiceInvokedAndWait { states ->
+ val actualRecommendations = states.recommendationContext.details.map { it.recommendation.content() }
assertThat(actualRecommendations).isEqualTo(expectedRecommendations)
- previews.map { it.detail }.forEachIndexed { index, context ->
+ states.recommendationContext.details.forEachIndexed { index, context ->
val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput)
val actualDiscarded = context.isDiscarded
assertThat(actualDiscarded).isEqualTo(expectedDiscarded)
@@ -52,11 +58,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
val typeahead = " recommendation"
- withCodeWhispererServiceInvokedAndWait { session ->
+ withCodeWhispererServiceInvokedAndWait { states ->
projectRule.fixture.type(typeahead)
- val previews = codewhispererService.getAllSuggestionsPreviewInfo()
- assertThat(previews[session.selectedIndex].typeahead).isEqualTo(typeahead)
- previews.map { it.detail }.forEachIndexed { index, actualContext ->
+ assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead)
+ states.recommendationContext.details.forEachIndexed { index, actualContext ->
val actualDiscarded = actualContext.isDiscarded
val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput + typeahead)
assertThat(actualDiscarded).isEqualTo(expectedDiscarded)
@@ -70,10 +75,9 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
addUserInputAfterInvocation(blankUserInput)
val userInput = blankUserInput.trimStart()
- withCodeWhispererServiceInvokedAndWait { session ->
- val previews = codewhispererService.getAllSuggestionsPreviewInfo()
- assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput)
- previews.map { it.detail }.forEachIndexed { _, actualContext ->
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput)
+ states.recommendationContext.details.forEachIndexed { _, actualContext ->
assertThat(actualContext.isDiscarded).isEqualTo(false)
}
}
@@ -85,10 +89,9 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
addUserInputAfterInvocation(userInputWithLeadingSpaces)
val userInput = userInputWithLeadingSpaces.trimStart()
- withCodeWhispererServiceInvokedAndWait { session ->
- val previews = codewhispererService.getAllSuggestionsPreviewInfo()
- assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput)
- previews.map { it.detail }.forEachIndexed { index, actualContext ->
+ withCodeWhispererServiceInvokedAndWait { states ->
+ assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput)
+ states.recommendationContext.details.forEachIndexed { index, actualContext ->
val actualDiscarded = actualContext.isDiscarded
val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput)
assertThat(actualDiscarded).isEqualTo(expectedDiscarded)
@@ -97,28 +100,33 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() {
}
private fun addUserInputAfterInvocation(userInput: String) {
+ val codewhispererServiceSpy = spy(codewhispererService)
val triggerTypeCaptor = argumentCaptor()
val editorCaptor = argumentCaptor()
val projectCaptor = argumentCaptor()
val psiFileCaptor = argumentCaptor()
- codewhispererService.stub {
+ val latencyContextCaptor = argumentCaptor()
+ codewhispererServiceSpy.stub {
onGeneric {
getRequestContext(
triggerTypeCaptor.capture(),
editorCaptor.capture(),
projectCaptor.capture(),
- psiFileCaptor.capture()
+ psiFileCaptor.capture(),
+ latencyContextCaptor.capture()
)
}.doAnswer {
- val requestContext = codewhispererService.getRequestContext(
+ val requestContext = codewhispererServiceSpy.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 894154880c..330ce9a092 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,7 +3,6 @@
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
@@ -47,13 +46,13 @@ 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.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.RequestContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy
import software.aws.toolkits.jetbrains.services.codewhisperer.util.UtgStrategy
import software.aws.toolkits.telemetry.CodewhispererCompletionType
@@ -70,9 +69,6 @@ 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"),
@@ -176,10 +172,10 @@ object CodeWhispererTestUtil {
const val leftContext_success_Iac = "# Create an S3 Bucket named CodeWhisperer in CloudFormation"
const val leftContext_failure_Iac = "Create an S3 Bucket named CodeWhisperer"
- fun pythonResponseWithToken(token: String): GenerateCompletionsResponse =
+ internal fun pythonResponseWithToken(token: String): GenerateCompletionsResponse =
pythonResponse.toBuilder().nextToken(token).build()
- fun generateMockCompletionDetail(content: String): Completion {
+ internal fun generateMockCompletionDetail(content: String): Completion {
val referenceInfo = getReferenceInfo()
return Completion.builder().content(content)
.references(
@@ -188,9 +184,9 @@ object CodeWhispererTestUtil {
.build()
}
- fun getReferenceInfo() = testReferenceInfoPair[Random.nextInt(testReferenceInfoPair.size)]
+ internal fun getReferenceInfo() = testReferenceInfoPair[Random.nextInt(testReferenceInfoPair.size)]
- fun generateMockCompletionDetail(
+ internal fun generateMockCompletionDetail(
content: String,
licenseName: String,
repository: String,
@@ -215,12 +211,6 @@ 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,
@@ -252,6 +242,21 @@ fun aRequestContext(
fileContextInfo = myFileContextInfo ?: aFileContextInfo(),
supplementalContextDeferred = supplementalContextDeferred,
null,
+ LatencyContext(
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextDouble(),
+ Random.nextDouble(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ Random.nextLong(),
+ aString()
+ ),
customizationArn = null
)
}
@@ -302,15 +307,14 @@ fun aRecommendationContext(): RecommendationContext {
details,
aString(),
aString(),
- VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)),
- 0
+ VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100))
)
}
/**
* util to generate a RecommendationContext and a SessionContext given expected decisions
*/
-fun aRecommendationContext(decisions: List): RecommendationContext {
+fun aRecommendationContextAndSessionContext(decisions: List): Pair {
val table = CodewhispererSuggestionState.values().associateWith { 0 }.toMutableMap()
decisions.forEach {
table[it]?.let { curCount -> table[it] = 1 + curCount }
@@ -328,8 +332,6 @@ fun aRecommendationContext(decisions: List): Recom
val completion = aCompletion()
DetailContext(aString(), completion, completion, false, Random.nextBoolean(), aString(), CodewhispererCompletionType.Line)
}
- toAdd.hasSeen = decision != CodewhispererSuggestionState.Unseen
- toAdd.isAccepted = decision == CodewhispererSuggestionState.Accept
details.add(toAdd)
}
@@ -338,11 +340,29 @@ fun aRecommendationContext(decisions: List): Recom
details,
aString(),
aString(),
- VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)),
- 0,
+ VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100))
)
- return recommendationContext
+ val selectedIndex = decisions.indexOfFirst { it == CodewhispererSuggestionState.Accept }.let {
+ if (it != -1) {
+ it
+ } else {
+ 0
+ }
+ }
+
+ val seen = mutableSetOf()
+ decisions.forEachIndexed { index, decision ->
+ if (decision != CodewhispererSuggestionState.Unseen) {
+ seen.add(index)
+ }
+ }
+
+ val sessionContext = SessionContext(
+ selectedIndex = selectedIndex,
+ seen = seen
+ )
+ return recommendationContext to sessionContext
}
fun aCompletion(content: String? = null, isEmpty: Boolean = false, referenceCount: Int? = null, importCount: Int? = null): Completion {
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
index 6516430bb5..de57180cba 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
@@ -42,6 +42,14 @@ class CodeWhispererFeatureConfigService {
featureConfigs[it.feature()] = FeatureContext(it.feature(), it.variation(), it.value())
}
+ // Only apply new auto-trigger UX to BID users
+ val isNewAutoTriggerUX = getNewAutoTriggerUX()
+ if (isNewAutoTriggerUX) {
+ calculateIfIamIdentityCenterConnection(project) {
+ featureConfigs.remove(NEW_AUTO_TRIGGER_UX)
+ }
+ }
+
val customizationArnOverride = featureConfigs[CUSTOMIZATION_ARN_OVERRIDE_NAME]?.value?.stringValue()
if (customizationArnOverride != null) {
// Double check if server-side wrongly returns a customizationArn to BID users
@@ -103,6 +111,8 @@ class CodeWhispererFeatureConfigService {
fun getCustomizationFeature(): FeatureContext? = getFeature(CUSTOMIZATION_ARN_OVERRIDE_NAME)
+ fun getNewAutoTriggerUX(): Boolean = getFeatureValueForKey(NEW_AUTO_TRIGGER_UX).stringValue() == "TREATMENT"
+
fun getInlineCompletion(): Boolean = getFeatureValueForKey(INLINE_COMPLETION).stringValue() == "TREATMENT"
// Get the feature value for the given key.
@@ -122,6 +132,7 @@ class CodeWhispererFeatureConfigService {
private const val TEST_FEATURE_NAME = "testFeature"
private const val INLINE_COMPLETION = "ProjectContextV2"
const val CUSTOMIZATION_ARN_OVERRIDE_NAME = "customizationArnOverride"
+ private const val NEW_AUTO_TRIGGER_UX = "newAutoTriggerUX"
private val LOG = getLogger()
// TODO: add real feature later
@@ -138,6 +149,11 @@ class CodeWhispererFeatureConfigService {
"customizationARN",
FeatureValue.builder().stringValue("").build()
),
+ NEW_AUTO_TRIGGER_UX to FeatureContext(
+ NEW_AUTO_TRIGGER_UX,
+ "CONTROL",
+ FeatureValue.builder().stringValue("CONTROL").build()
+ ),
INLINE_COMPLETION to FeatureContext(
INLINE_COMPLETION,
"CONTROL",