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",