diff --git a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt index 2bccc07914e..c81e78dfe37 100644 --- a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt +++ b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt @@ -59,5 +59,5 @@ fun Project.buildMetadata() = } catch(e: Exception) { logger.warn("Could not determine current commit", e) - "unknownCommit" + "beta.20240910" } diff --git a/gradle.properties b/gradle.properties index 1e7e0e3c7ae..51419b48f45 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Toolkit Version -toolkitVersion=3.30-SNAPSHOT +toolkitVersion=99.99.2 # Publish Settings publishToken= diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml index 8b7e5614ddb..e2bc26409fb 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml +++ b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml @@ -27,6 +27,8 @@ + diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt index 3b4979572cb..a7dff2e1704 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt @@ -27,6 +27,7 @@ interface ActionProvider { val sendFeedback: T val connectOnGithub: T val documentation: T + val switchToMarketplace: T } @SuppressWarnings("UnusedParameter") @@ -84,3 +85,8 @@ fun buildActionListForConnectHelp(actionProvider: ActionProvider): List buildActionListForBeta(actionProvider: ActionProvider): List = + buildList { + add(actionProvider.switchToMarketplace) + } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt index 15cf836bc46..c9a5c06c595 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 @@ -134,6 +134,16 @@ data class SessionContext( var hasAccepted: Boolean = false ) : Disposable { private var isDisposed = false + init { + project.messageBus.connect().subscribe( + CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER, + object : CodeWhispererIntelliSenseOnHoverListener { + override fun onEnter() { + CodeWhispererPopupManager.getInstance().bringSuggestionInlayToFront(editor, popup, opposite = true) + } + } + ) + } @RequiresEdt override fun dispose() { @@ -142,6 +152,7 @@ data class SessionContext( hasAccepted, CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) } ) + setIntelliSensePopupAlpha(editor, 0f) CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false) if (hasAccepted) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt new file mode 100644 index 00000000000..3a46375e250 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/plugin/QBetaPluginManagementPolicy.kt @@ -0,0 +1,22 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.plugin + +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.ide.plugins.PluginManagementPolicy +import com.intellij.ide.plugins.org.PluginManagerFilters + +// Specifically for "Switch Back to Marketplace" action because the default one doesn't support downgrade +class QBetaPluginManagementPolicy : PluginManagementPolicy { + override fun canEnablePlugin(descriptor: IdeaPluginDescriptor?): Boolean = + descriptor?.let { PluginManagerFilters.getInstance().allowInstallingPlugin(it) } ?: true + + override fun canInstallPlugin(descriptor: IdeaPluginDescriptor?): Boolean = canEnablePlugin(descriptor) + + override fun isDowngradeAllowed(localDescriptor: IdeaPluginDescriptor?, remoteDescriptor: IdeaPluginDescriptor?): Boolean = true + + override fun isInstallFromDiskAllowed(): Boolean = PluginManagerFilters.getInstance().allowInstallFromDisk() + + override fun isUpgradeAllowed(localDescriptor: IdeaPluginDescriptor?, remoteDescriptor: IdeaPluginDescriptor?): Boolean = true +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt index fcf2279f478..8faf681c9dd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupComponents.kt @@ -6,11 +6,16 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.idea.AppMode +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionToolbar import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.keymap.MacKeymapUtil +import com.intellij.openapi.util.SystemInfo import com.intellij.ui.IdeBorderFactory import com.intellij.ui.components.ActionLink import com.intellij.util.ui.UIUtil @@ -43,10 +48,38 @@ import javax.swing.JPanel class CodeWhispererPopupComponents { val prevButton = createNavigationButton( - message("codewhisperer.popup.button.prev", POPUP_DIM_HEX) + message( + "codewhisperer.popup.button.prev", + POPUP_DIM_HEX, + run { + // TODO: Doesn't reflect dynamically if users change but didn't restart IDE + val shortcut = ActionManager.getInstance().getAction("codewhisperer.inline.navigate.previous") + .shortcutSet.shortcuts.first() + val keyStroke = (shortcut as KeyboardShortcut).firstKeyStroke + if (SystemInfo.isMac) { + MacKeymapUtil.getKeyStrokeText(keyStroke, " ", true) + } else { + KeymapUtil.getKeystrokeText(keyStroke) + } + } + ) ) val nextButton = createNavigationButton( - message("codewhisperer.popup.button.next", POPUP_DIM_HEX) + message( + "codewhisperer.popup.button.next", + POPUP_DIM_HEX, + run { + // TODO: Doesn't reflect dynamically if users change but didn't restart IDE + val shortcut = ActionManager.getInstance().getAction("codewhisperer.inline.navigate.next") + .shortcutSet.shortcuts.first() + val keyStroke = (shortcut as KeyboardShortcut).firstKeyStroke + if (SystemInfo.isMac) { + MacKeymapUtil.getKeyStrokeText(keyStroke, " ", true) + } else { + KeymapUtil.getKeystrokeText(keyStroke) + } + } + ) ).apply { preferredSize = prevButton.preferredSize } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt index 01cb98be42a..9dadf7e1134 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,16 +3,12 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup -import com.intellij.codeInsight.hint.ParameterInfoController import com.intellij.codeInsight.lookup.LookupManager import com.intellij.idea.AppMode import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_BACKSPACE import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_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 @@ -33,11 +29,11 @@ import com.intellij.openapi.editor.event.EditorMouseEvent import com.intellij.openapi.editor.event.EditorMouseMotionListener import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key import com.intellij.openapi.wm.WindowManager import com.intellij.ui.ComponentUtil import com.intellij.ui.awt.RelativePoint @@ -66,7 +62,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.Cod 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.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 @@ -75,8 +70,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.Co 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.service.CodeWhispererService.Companion -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 @@ -311,6 +304,20 @@ class CodeWhispererPopupManager { 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() @@ -489,6 +496,20 @@ class CodeWhispererPopupManager { 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 { + sessionContext.project.messageBus.syncPublisher( + CodeWhispererService.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER, + ).onEnter() + } + super.mouseMoved(e) + } + } + editor.addEditorMouseMotionListener(suggestionHoverEnterListener, sessionContext) } private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) { @@ -565,10 +586,6 @@ class CodeWhispererPopupManager { } } - fun hasConflictingPopups(editor: Editor): Boolean = - ParameterInfoController.existsWithVisibleHintForEditor(editor, true) || - LookupManager.getActiveLookup(editor) != null - fun findNewSelectedIndex(isReverse: Boolean, selectedIndex: Int): Int { val start = if (selectedIndex == -1) 0 else selectedIndex val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt index 7bc77295a99..6a7a03e132e 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererAcceptButtonActionListener.kt @@ -4,14 +4,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners import com.intellij.openapi.application.ApplicationManager -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import java.awt.event.ActionEvent -class CodeWhispererAcceptButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) { +class CodeWhispererAcceptButtonActionListener(sessionContext: SessionContext) : CodeWhispererActionListener(sessionContext) { override fun actionPerformed(e: ActionEvent?) { ApplicationManager.getApplication().messageBus.syncPublisher( CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED - ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext) + ).beforeAccept(sessionContext) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt index 68c578d092e..d1dcbcdf1cd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerService.kt @@ -4,7 +4,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.service import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service @@ -26,6 +25,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmi import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.INVOCATION_DELAY import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState import software.aws.toolkits.telemetry.CodewhispererTriggerType @@ -42,6 +42,8 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa private var lastInvocationTime: Instant? = null private var lastInvocationLineNum: Int? = null + private var lastCharTypedTime: Instant? = null + private var lastTrigger: Job? = null init { scheduleReset() @@ -55,9 +57,6 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa fun tryInvokeAutoTrigger(editor: Editor, triggerType: CodeWhispererAutomatedTriggerType): Job? { // only needed for Classifier group, thus calculate it lazily val classifierResult: ClassifierResult by lazy { shouldTriggerClassifier(editor, triggerType.telemetryType) } - val language = runReadAction { - FileDocumentManager.getInstance().getFile(editor.document)?.programmingLanguage() - } ?: CodeWhispererUnknownLanguage.INSTANCE // we need classifier result for any type of triggering for classifier group for supported languages triggerType.calculationResult = classifierResult.calculatedResult @@ -84,8 +83,12 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa if (!CodeWhispererService.getInstance().canDoInvocation(editor, CodewhispererTriggerType.AutoTrigger)) { return null } + if (hasEnoughDelaySinceLastTrigger()) { + lastTrigger?.cancel() + } lastInvocationTime = Instant.now() + lastCharTypedTime = Instant.now() lastInvocationLineNum = runReadAction { editor.caretModel.visualPosition.line } val latencyContext = LatencyContext().apply { @@ -95,32 +98,23 @@ class CodeWhispererAutoTriggerService : CodeWhispererAutoTriggerHandler, Disposa val coroutineScope = applicationCoroutineScope() - return when (triggerType) { - is CodeWhispererAutomatedTriggerType.IdleTime -> run { - coroutineScope.launch { - // TODO: potential race condition between hasExistingInvocation and entering edt - // but in that case we will just return in performAutomatedTriggerAction - while (!CodeWhispererInvocationStatus.getInstance().hasEnoughDelayToInvokeCodeWhisperer() || - CodeWhispererInvocationStatus.getInstance().hasExistingInvocation() - ) { - if (!isActive) return@launch - delay(CodeWhispererConstants.IDLE_TIME_CHECK_INTERVAL) - } - runInEdt { - if (CodeWhispererInvocationStatus.getInstance().isPopupActive()) return@runInEdt - performAutomatedTriggerAction(editor, CodeWhispererAutomatedTriggerType.IdleTime(), latencyContext) - } + return run { + coroutineScope.launch(EDT) { + while (!hasEnoughDelaySinceLastTrigger()) { + if (!isActive) return@launch + delay(CodeWhispererConstants.IDLE_TIME_CHECK_INTERVAL) } - } - else -> run { - coroutineScope.launch(EDT) { - performAutomatedTriggerAction(editor, triggerType, latencyContext) - } + performAutomatedTriggerAction(editor, triggerType, latencyContext) + }.also { + lastTrigger = it } } } + private fun hasEnoughDelaySinceLastTrigger(): Boolean = + lastCharTypedTime == null || lastCharTypedTime?.plusMillis(INVOCATION_DELAY)?.isBefore(Instant.now()) == true + private fun scheduleReset() { if (!alarm.isDisposed) { alarm.addRequest({ resetPreviousStates() }, Duration.ofSeconds(120).toMillis()) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt index 4c58139b354..5bf5852bcc8 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt @@ -27,6 +27,7 @@ class CodeWhispererInvocationStatus { fun startInvocation() { isInvokingService.set(true) + ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_INVOCATION_STATE_CHANGED).invocationStateChanged(true) LOG.debug { "Starting CodeWhisperer invocation" } } 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 56351791ac5..f1ca6f3abc8 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 @@ -101,6 +101,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val ongoingRequestsContext = mutableMapOf() private var jobId = 0 private var sessionContext: SessionContext? = null + var isBetaExpired: Boolean = false init { Disposer.register(this, codeInsightSettingsFacade) @@ -155,6 +156,11 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { } } + if (isBetaExpired && triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { + showCodeWhispererInfoHint(editor, "Current beta period ended, please switch to the marketplace version") + return + } + latencyContext.credentialFetchingEnd = System.nanoTime() val psiFile = runReadAction { PsiDocumentManager.getInstance(project).getPsiFile(editor.document) } @@ -233,6 +239,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { var lastRecommendationIndex = -1 + val line = requestContext.fileContextInfo.caretContext.leftContextOnCurrentLine + println("triggering, last char $line, jobId: $currentJobId") val job = coroutineScope.launch { try { val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( @@ -813,6 +821,10 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { "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() @@ -899,3 +911,7 @@ data class ResponseContext( interface CodeWhispererCodeCompletionServiceListener { fun onSuccess(fileContextInfo: FileContextInfo) {} } + +interface CodeWhispererIntelliSenseOnHoverListener { + fun onEnter() {} +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index 9db80018184..8e10475fe0a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -4,13 +4,17 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.settings import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager import com.intellij.openapi.options.BoundConfigurable +import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.SearchableConfigurable +import com.intellij.openapi.options.ex.Settings import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.components.ActionLink import com.intellij.ui.dsl.builder.bindIntText import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.EdtExecutorService import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType @@ -18,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhisp import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled import software.aws.toolkits.resources.message import java.awt.Font +import java.util.concurrent.TimeUnit // As the connection is project-level, we need to make this project-level too (we have different config for Sono vs SSO users) class CodeWhispererConfigurable(private val project: Project) : @@ -87,6 +92,20 @@ class CodeWhispererConfigurable(private val project: Project) : bindSelected(codeWhispererSettings::isImportAdderEnabled, codeWhispererSettings::toggleImportAdder) }.comment(message("aws.settings.codewhisperer.automatic_import_adder.tooltip")) } + + row { + link("Configure inline suggestion keybindings") { e -> + val settings = DataManager.getInstance().getDataContext(e.source as ActionLink).getData(Settings.KEY) ?: return@link + val configurable: Configurable = settings.find("preferences.keymap") ?: return@link + + settings.select(configurable, Q_INLINE_KEYBINDING_SEARCH_TEXT) + + // workaround for certain cases for sometimes the string is not input there + EdtExecutorService.getScheduledExecutorInstance().schedule({ + settings.select(configurable, Q_INLINE_KEYBINDING_SEARCH_TEXT) + }, 500, TimeUnit.MILLISECONDS) + } + } } group(message("aws.settings.codewhisperer.group.q_chat")) { @@ -109,7 +128,7 @@ class CodeWhispererConfigurable(private val project: Project) : intTextField( range = IntRange(0, 50) ).bindIntText(codeWhispererSettings::getProjectContextIndexThreadCount, codeWhispererSettings::setProjectContextIndexThreadCount) - .align(AlignX.FILL).apply { + .apply { connect.subscribe( ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { @@ -119,14 +138,14 @@ class CodeWhispererConfigurable(private val project: Project) : } ) enabled(invoke) - }.comment(message("aws.settings.codewhisperer.project_context_index_thread.tooltip")) + }.comment(message("aws.settings.codewhisperer.project_context_index_thread.tooltip"), maxLineLength = 47) } row(message("aws.settings.codewhisperer.project_context_index_max_size")) { intTextField( range = IntRange(1, 250) ).bindIntText(codeWhispererSettings::getProjectContextIndexMaxSize, codeWhispererSettings::setProjectContextIndexMaxSize) - .align(AlignX.FILL).apply { + .apply { connect.subscribe( ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { @@ -183,4 +202,8 @@ class CodeWhispererConfigurable(private val project: Project) : } } } + + companion object { + private const val Q_INLINE_KEYBINDING_SEARCH_TEXT = "inline suggestion" + } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt index 749e5e273d0..24105ace86f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererSettings.kt @@ -94,6 +94,13 @@ class CodeWhispererSettings : PersistentStateComponent() - - detailContexts.forEachIndexed { index, detailContext -> - val suggestionState = recordSuggestionState( - index, - sessionContext.selectedIndex, - sessionContext.seen.contains(index), - hasUserAccepted, - detailContext.isDiscarded, - detailContext.recommendation.content().isEmpty() - ) - sendUserDecisionEvent(requestContext, responseContext, detailContext, index, suggestionState, detailContexts.size) + CodeWhispererService.getInstance().getAllPaginationSessions().forEach { (jobId, state) -> + if (state == null) return@forEach + val decisions = mutableListOf() + val details = state.recommendationContext.details - decisions.add(suggestionState) - } + details.forEachIndexed { index, detail -> + val suggestionState = recordSuggestionState(detail, hasUserAccepted) + sendUserDecisionEvent(state.requestContext, state.responseContext, detail, index, suggestionState, details.size) - with(aggregateUserDecision(decisions)) { - // the order of the following matters - // step 1, send out current decision - previousUserTriggerDecisionTimestamp = Instant.now() - - val referenceCount = if (hasUserAccepted && detailContexts[sessionContext.selectedIndex].recommendation.hasReferences()) 1 else 0 - val acceptedContent = - if (hasUserAccepted) { - detailContexts[sessionContext.selectedIndex].recommendation.content() - } else { - "" + decisions.add(suggestionState) + } + LOG.debug { "jobId: $jobId, userDecisions: [${decisions.joinToString(", ")}]" } + + with(aggregateUserDecision(decisions)) { + // the order of the following matters + // step 1, send out current decision + LOG.debug { "jobId: $jobId, userTriggerDecision: $this" } + previousUserTriggerDecisionTimestamp = Instant.now() + + val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo() + val recommendation = + if (hasUserAccepted) { + previews[sessionContext.selectedIndex].detail.recommendation + } else { + Completion.builder().content("").references(emptyList()).build() + } + val referenceCount = if (hasUserAccepted && recommendation.hasReferences()) 1 else 0 + val acceptedContent = if (hasUserAccepted) recommendation.content() else "" + val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size + val acceptedCharCount = acceptedContent.length + sendUserTriggerDecisionEvent( + sessionContext, + state.requestContext, + state.responseContext, + state.recommendationContext, + this, + popupShownTime, + referenceCount, + generatedLineCount, + acceptedCharCount + ) + + // step 2, put current decision into queue for later reference + if (this != CodewhispererSuggestionState.Ignore && this != CodewhispererSuggestionState.Unseen) { + val previousState = CodewhispererPreviousSuggestionState.from(this.toString()) + // we need this as well because AutoTriggerService will reset the queue periodically + previousUserTriggerDecisions.add(previousState) + CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(previousState) } - val generatedLineCount = if (acceptedContent.isEmpty()) 0 else acceptedContent.split("\n").size - val acceptedCharCount = acceptedContent.length - sendUserTriggerDecisionEvent( - requestContext, - responseContext, - recommendationContext, - CodewhispererSuggestionState.from(this.toString()), - popupShownTime, - referenceCount, - generatedLineCount, - acceptedCharCount - ) - - // step 2, put current decision into queue for later reference - previousUserTriggerDecisions.add(this) - // we need this as well because AutotriggerService will reset the queue periodically - CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(this) + } } } @@ -446,74 +452,91 @@ class CodeWhispererTelemetryService { * - Accept if there is an Accept * - Reject if there is a Reject * - Empty if all decisions are Empty + * - Ignore if at least one suggestion is seen and there's an accept for another trigger in the same display session + * - Unseen if the whole trigger is not seen (but has valid suggestions) * - Record the accepted suggestion index * - Discard otherwise */ - fun aggregateUserDecision(decisions: List): CodewhispererPreviousSuggestionState { + fun aggregateUserDecision(decisions: List): CodewhispererSuggestionState { var isEmpty = true + var isUnseen = true + var isDiscard = true for (decision in decisions) { if (decision == CodewhispererSuggestionState.Accept) { - return CodewhispererPreviousSuggestionState.Accept + return CodewhispererSuggestionState.Accept } else if (decision == CodewhispererSuggestionState.Reject) { - return CodewhispererPreviousSuggestionState.Reject - } else if (decision != CodewhispererSuggestionState.Empty) { + return CodewhispererSuggestionState.Reject + } else if (decision == CodewhispererSuggestionState.Unseen) { + isEmpty = false + isDiscard = false + } else if (decision == CodewhispererSuggestionState.Ignore) { + isUnseen = false + isEmpty = false + isDiscard = false + } else if (decision == CodewhispererSuggestionState.Discard) { isEmpty = false } } return if (isEmpty) { - CodewhispererPreviousSuggestionState.Empty + CodewhispererSuggestionState.Empty + } else if (isDiscard) { + CodewhispererSuggestionState.Discard + } else if (isUnseen) { + CodewhispererSuggestionState.Unseen } else { - CodewhispererPreviousSuggestionState.Discard + CodewhispererSuggestionState.Ignore } } - fun sendPerceivedLatencyEvent( - requestId: String, - requestContext: RequestContext, - responseContext: ResponseContext, - latency: Double, - ) { - val (project, _, triggerTypeInfo) = requestContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.perceivedLatency( - project = project, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerTypeInfo.triggerType, - duration = latency, - passive = true, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } - - fun sendClientComponentLatencyEvent(states: InvocationContext) { - val requestContext = states.requestContext - val responseContext = states.responseContext - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.clientComponentLatency( - project = requestContext.project, - codewhispererSessionId = responseContext.sessionId, - codewhispererRequestId = requestContext.latencyContext.firstRequestId, - codewhispererFirstCompletionLatency = requestContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreprocessingLatency = requestContext.latencyContext.getCodeWhispererPreprocessingLatency(), - codewhispererEndToEndLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency(), - codewhispererAllCompletionsLatency = requestContext.latencyContext.getCodeWhispererAllCompletionsLatency(), - codewhispererPostprocessingLatency = requestContext.latencyContext.getCodeWhispererPostprocessingLatency(), - codewhispererCredentialFetchingLatency = requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) - } + // TODO: Decide the scope of this telemetry +// fun sendPerceivedLatencyEvent( +// requestId: String, +// requestContext: RequestContext, +// responseContext: ResponseContext, +// latency: Double, +// ) { +// val (project, _, triggerTypeInfo) = requestContext +// val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() +// val startUrl = getConnectionStartUrl(requestContext.connection) +// CodewhispererTelemetry.perceivedLatency( +// project = project, +// codewhispererCompletionType = CodewhispererCompletionType.Line, +// codewhispererLanguage = codewhispererLanguage, +// codewhispererRequestId = requestId, +// codewhispererSessionId = responseContext.sessionId, +// codewhispererTriggerType = triggerTypeInfo.triggerType, +// duration = latency, +// passive = true, +// credentialStartUrl = startUrl, +// codewhispererCustomizationArn = requestContext.customizationArn, +// ) +// } + + // TODO: decide the scope of this telemetry +// fun sendClientComponentLatencyEvent(states: InvocationContext) { +// val requestContext = states.requestContext +// val responseContext = states.responseContext +// val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() +// val startUrl = getConnectionStartUrl(requestContext.connection) +// CodewhispererTelemetry.clientComponentLatency( +// project = requestContext.project, +// codewhispererSessionId = responseContext.sessionId, +// codewhispererRequestId = requestContext.latencyContext.firstRequestId, +// codewhispererFirstCompletionLatency = requestContext.latencyContext.paginationFirstCompletionTime, +// codewhispererPreprocessingLatency = requestContext.latencyContext.getCodeWhispererPreprocessingLatency(), +// codewhispererEndToEndLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency(), +// codewhispererAllCompletionsLatency = requestContext.latencyContext.getCodeWhispererAllCompletionsLatency(), +// codewhispererPostprocessingLatency = requestContext.latencyContext.getCodeWhispererPostprocessingLatency(), +// codewhispererCredentialFetchingLatency = requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency(), +// codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, +// codewhispererCompletionType = CodewhispererCompletionType.Line, +// codewhispererLanguage = codewhispererLanguage, +// credentialStartUrl = startUrl, +// codewhispererCustomizationArn = requestContext.customizationArn, +// ) +// } fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) { // Project instance is not needed. We look at these metrics for each clientId. @@ -521,21 +544,17 @@ class CodeWhispererTelemetryService { } fun recordSuggestionState( - index: Int, - selectedIndex: Int, - hasSeen: Boolean, + detail: DetailContext, hasUserAccepted: Boolean, - isDiscarded: Boolean, - isEmpty: Boolean ): CodewhispererSuggestionState = - if (isEmpty) { + if (detail.recommendation.content().isEmpty()) { CodewhispererSuggestionState.Empty - } else if (isDiscarded) { + } else if (detail.isDiscarded) { CodewhispererSuggestionState.Discard - } else if (!hasSeen) { + } else if (!detail.hasSeen) { CodewhispererSuggestionState.Unseen } else if (hasUserAccepted) { - if (selectedIndex == index) { + if (detail.isAccepted) { CodewhispererSuggestionState.Accept } else { CodewhispererSuggestionState.Ignore diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt index 4eb9980e216..519cd599bed 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt @@ -27,7 +27,7 @@ object CodeWhispererConstants { const val LEFT_CONTEXT_ON_CURRENT_LINE = 50 const val POPUP_INFO_TEXT_SIZE = 11f const val POPUP_BUTTON_TEXT_SIZE = 12f - const val POPUP_DELAY: Long = 250 + const val POPUP_DELAY: Long = 50 const val POPUP_DELAY_CHECK_INTERVAL: Long = 25 const val IDLE_TIME_CHECK_INTERVAL: Long = 25 const val SUPPLEMENTAL_CONTEXT_TIMEOUT = 50L @@ -36,9 +36,9 @@ object CodeWhispererConstants { val AWSTemplateKeyWordsRegex = Regex("(AWSTemplateFormatVersion|Resources|AWS::|Description)") val AWSTemplateCaseInsensitiveKeyWordsRegex = Regex("(cloudformation|cfn|template|description)") - // TODO: this is currently set to 2050 to account for the server side 0.5 TPS and and extra 50 ms buffer to - // avoid ThrottlingException as much as possible. - const val INVOCATION_INTERVAL: Long = 2050 + // TODO: this is currently set to 0 to trigger with 0ms delay and rely on ML trigger to determine which characters to trigger at + // We will monitor service side resource utilization and throttling. + const val INVOCATION_DELAY: Long = 0 const val Q_CUSTOM_LEARN_MORE_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/customizations.html" const val Q_SUPPORTED_LANG_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html" diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt index 7f48c50142a..ada04ea915d 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.util +import com.intellij.codeInsight.lookup.LookupManager import com.intellij.ide.BrowserUtil import com.intellij.notification.NotificationAction import com.intellij.openapi.application.ApplicationManager @@ -12,6 +13,8 @@ import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.ComponentUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -307,6 +310,12 @@ object CodeWhispererUtil { } } } + + fun setIntelliSensePopupAlpha(editor: Editor, alpha: Float) { + ComponentUtil.getWindow(LookupManager.getActiveLookup(editor)?.component)?.let { + WindowManager.getInstance().setAlphaModeRatio(it, alpha) + } + } } enum class CaretMovement { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt index 4026c090a72..a785644614f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererAcceptTest.kt @@ -8,11 +8,6 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT -import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TAB -import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -22,6 +17,9 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.stub import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_ACCEPT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_NEXT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_PREV import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.generateMockCompletionDetail import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaFileName import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.javaResponse @@ -125,9 +123,9 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer keyboard shortcuts should be prioritized to be executed`() { - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_TAB) - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_RIGHT) - testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_EDITOR_MOVE_CARET_LEFT) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_ACCEPT) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_PREV) + testCodeWhispererKeyboardShortcutShouldBePrioritized(ACTION_KEY_NAV_NEXT) } private fun testCodeWhispererKeyboardShortcutShouldBePrioritized(actionId: String) { @@ -160,8 +158,8 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { // move the cursor to the correct trigger point (...void main) projectRule.fixture.editor.caretModel.moveToOffset(47) } - withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + withCodeWhispererServiceInvokedAndWait { session -> + val recommendation = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].detail.reformatted.content() val editor = projectRule.fixture.editor val expectedContext = buildContextWithRecommendation(recommendation + remaining) val startOffset = editor.caretModel.offset @@ -176,8 +174,9 @@ class CodeWhispererAcceptTest : CodeWhispererTestBase() { private fun acceptHelper(useKeyboard: Boolean) { if (useKeyboard) { - EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_TAB) - .execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + ActionManager.getInstance().getAction(ACTION_KEY_ACCEPT).actionPerformed( + AnActionEvent.createFromDataContext("test", null, DataContext.EMPTY_CONTEXT) + ) } else { popupManagerSpy.popupComponents.acceptButton.doClick() } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt index e76a34d2e45..ae72562c7e7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt @@ -82,6 +82,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWh import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService @@ -238,10 +239,10 @@ class CodeWhispererClientAdaptorTest { @Test fun sendUserTriggerDecisionTelemetry() { - val mockModelConfiguraotr = mock { + val mockModelConfigurator = mock { on { activeCustomization(any()) } doReturn CodeWhispererCustomization("fake-arn", "fake-name") } - ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfiguraotr, disposableRule.disposable) + ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockModelConfigurator, disposableRule.disposable) val file = projectRule.fixture.addFileToProject("main.java", "public class Main {}") runInEdtAndWait { @@ -252,10 +253,14 @@ class CodeWhispererClientAdaptorTest { projectRule.fixture.editor, projectRule.project, file, - LatencyContext(codewhispererEndToEndStart = 0, codewhispererEndToEndEnd = 20000000) ) sut.sendUserTriggerDecisionTelemetry( + SessionContext( + projectRule.project, + projectRule.fixture.editor, + latencyContext = LatencyContext(codewhispererEndToEndStart = 0, codewhispererEndToEndEnd = 20000000) + ), requestContext, ResponseContext("fake-session-id"), CodewhispererCompletionType.Line, @@ -374,6 +379,7 @@ class CodeWhispererClientAdaptorTest { fun `sendTelemetryEvent for userTriggerDecision respects telemetry optin status, for SSO users`() { sendTelemetryEventOptOutCheckHelper { sut.sendUserTriggerDecisionTelemetry( + SessionContext(projectRule.project, mock(), latencyContext = LatencyContext()), aRequestContext(projectRule.project), aResponseContext(), aCompletionType(), diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt index 8979a11fb32..0ff94e68fc4 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt @@ -53,6 +53,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.FileContextInfo import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.SupplementalContextInfo @@ -174,12 +175,11 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov } }, null, - mock(), aString() ) val responseContext = ResponseContext("sessionId") val recommendationContext = RecommendationContext( - listOf( + mutableListOf( DetailContext( "requestId", pythonResponse.completions()[0], @@ -192,10 +192,11 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov ), "x, y", "x, y", - mock() + mock(), + 0 ) - invocationContext = InvocationContext(requestContext, responseContext, recommendationContext, mock()) - sessionContext = SessionContext() + invocationContext = InvocationContext(requestContext, responseContext, recommendationContext) + sessionContext = SessionContext(project, fixture.editor, latencyContext = LatencyContext()) // it is needed because referenceManager is listening to CODEWHISPERER_USER_ACTION_PERFORMED topic project.replaceService(CodeWhispererCodeReferenceManager::class.java, mock(), disposableRule.disposable) @@ -338,6 +339,7 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_USER_ACTION_PERFORMED).afterAccept( invocationContext, mock(), + SessionContext(project, fixture.editor, latencyContext = LatencyContext()), rangeMarkerMock ) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt index 1e3d42ffff0..8f2e184d057 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererNavigationTest.kt @@ -3,11 +3,13 @@ package software.aws.toolkits.jetbrains.services.codewhisperer +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.editor.actionSystem.EditorActionManager import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_NEXT +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.ACTION_KEY_NAV_PREV import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse import javax.swing.JButton @@ -34,10 +36,10 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { } private fun testNavigation(isReverse: Boolean, useKeyboard: Boolean = false) { - withCodeWhispererServiceInvokedAndWait { + withCodeWhispererServiceInvokedAndWait { session -> val indexChange = if (isReverse) -1 else 1 - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(0) + assertThat(session.selectedIndex).isEqualTo(0) val expectedCount = pythonResponse.completions().size var expectedSelectedIndex: Int @@ -58,7 +60,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { } } - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex) + assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex) assertThat(oppositeButton.isEnabled).isEqualTo(false) repeat(expectedCount - 1) { @@ -66,7 +68,7 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { navigateHelper(isReverse, useKeyboard) assertThat(oppositeButton.isEnabled).isEqualTo(true) expectedSelectedIndex = (expectedSelectedIndex + indexChange) % expectedCount - assertThat(popupManagerSpy.sessionContext.selectedIndex).isEqualTo(expectedSelectedIndex) + assertThat(session.selectedIndex).isEqualTo(expectedSelectedIndex) checkRecommendationInfoLabelText(expectedSelectedIndex + 1, expectedCount) } assertThat(navigationButton.isEnabled).isEqualTo(false) @@ -75,13 +77,13 @@ class CodeWhispererNavigationTest : CodeWhispererTestBase() { private fun navigateHelper(isReverse: Boolean, useKeyboard: Boolean) { if (useKeyboard) { - val actionHandler = EditorActionManager.getInstance() + val actionHandler = ActionManager.getInstance() if (isReverse) { - val leftArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT) - leftArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + val leftArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_PREV) + leftArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null, DataContext.EMPTY_CONTEXT)) } else { - val rightArrowHandler = actionHandler.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT) - rightArrowHandler.execute(projectRule.fixture.editor, null, DataContext.EMPTY_CONTEXT) + val rightArrowHandler = actionHandler.getAction(ACTION_KEY_NAV_NEXT) + rightArrowHandler.actionPerformed(AnActionEvent.createFromDataContext("", null, DataContext.EMPTY_CONTEXT)) } } else { if (isReverse) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt index f186740f1e8..0a050493a56 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererReferencesTest.kt @@ -56,8 +56,8 @@ class CodeWhispererReferencesTest : CodeWhispererTestBase() { } } - withCodeWhispererServiceInvokedAndWait { states -> - states.recommendationContext.details.forEach { + withCodeWhispererServiceInvokedAndWait { session -> + codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail }.forEach { assertThat(it.recommendation.references().isEmpty()).isEqualTo(invalid) } popupManagerSpy.popupComponents.acceptButton.doClick() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt index e4202cba6b3..5d83170927f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRightContextTest.kt @@ -21,8 +21,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { fun `test recommendation equal to right context should not show recommendation`() { val rightContext = pythonResponse.completions()[0].content() setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] + withCodeWhispererServiceInvokedAndWait { + val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail assertThat(firstRecommendation.isDiscarded).isEqualTo(true) } } @@ -31,12 +31,12 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { fun `test right context resolution will remove reference span if reference is the same as right context`() { val rightContext = pythonResponse.completions()[0].content() setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] + withCodeWhispererServiceInvokedAndWait { _ -> + val firstRecommendation = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail assertThat(firstRecommendation.isDiscarded).isEqualTo(true) - val details = states.recommendationContext.details - details.forEach { - assertThat(it.reformatted.references().isEmpty()) + val details = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail } + details.forEach { detail -> + assertThat(detail.reformatted.references().isEmpty()) } } } @@ -47,10 +47,9 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { val lastNewLineIndex = firstRecommendationContent.lastIndexOf('\n') val rightContext = firstRecommendationContent.substring(lastNewLineIndex) setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(false) - val firstDetail = states.recommendationContext.details[0] + withCodeWhispererServiceInvokedAndWait { + val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + assertThat(firstDetail.isDiscarded).isEqualTo(false) val span = firstDetail.reformatted.references()[0].recommendationContentSpan() assertThat(span.start()).isEqualTo(0) assertThat(span.end()).isEqualTo(lastNewLineIndex) @@ -84,9 +83,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { } withCodeWhispererServiceInvokedAndWait { states -> - val firstRecommendation = states.recommendationContext.details[0] - assertThat(firstRecommendation.isDiscarded).isEqualTo(false) - val firstDetail = states.recommendationContext.details[0] + val firstDetail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + assertThat(firstDetail.isDiscarded).isEqualTo(false) val span = firstDetail.reformatted.references()[0].recommendationContentSpan() assertThat(span.start()).isEqualTo(0) assertThat(span.end()).isEqualTo(lastNewLineIndex) @@ -101,10 +99,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { val remaining = firstRecommendation.substring(0, remainingLength) val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining) + withCodeWhispererServiceInvokedAndWait { session -> + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining) popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) + assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) } } @@ -115,8 +113,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { val remainingLength = Random.nextInt(newLineIndex, firstRecommendation.length) val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation) + withCodeWhispererServiceInvokedAndWait { + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.recommendation.content()).isEqualTo(firstRecommendation) } } @@ -128,10 +126,10 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { val remaining = firstRecommendation.substring(0, remainingLength) val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) + "test" setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].reformatted.content()).isEqualTo(remaining) + withCodeWhispererServiceInvokedAndWait { session -> + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.reformatted.content()).isEqualTo(remaining) popupManagerSpy.popupComponents.acceptButton.doClick() - assertThat(states.requestContext.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) + assertThat(session.editor.document.text).isEqualTo(pythonTestLeftContext + remaining + rightContext) } } @@ -142,8 +140,8 @@ class CodeWhispererRightContextTest : CodeWhispererTestBase() { val remainingLength = Random.nextInt(newLineIndex, firstRecommendation.length) val rightContext = pythonResponse.completions()[0].content().substring(remainingLength) + "test" setFileContext(pythonFileName, pythonTestLeftContext, rightContext) - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.details[0].recommendation.content()).isEqualTo(firstRecommendation) + withCodeWhispererServiceInvokedAndWait { + assertThat(codewhispererService.getAllSuggestionsPreviewInfo()[0].detail.recommendation.content()).isEqualTo(firstRecommendation) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt index 4bff4965c3d..e55cd93ebe7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt @@ -12,6 +12,7 @@ import com.intellij.testFramework.runInEdtAndWait import kotlinx.coroutines.async import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Rule @@ -93,6 +94,11 @@ class CodeWhispererServiceTest { projectRule.project.replaceService(AwsConnectionManager::class.java, mock(), disposableRule.disposable) } + @After + fun tearDown() { + sut.disposeDisplaySession(false) + } + @Test fun `getRequestContext should have supplementalContext and customizatioArn if they're present`() { whenever(customizationConfig.activeCustomization(projectRule.project)).thenReturn( @@ -124,8 +130,7 @@ class CodeWhispererServiceTest { TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), projectRule.fixture.editor, projectRule.project, - file, - LatencyContext() + file ) runTest { @@ -149,8 +154,7 @@ class CodeWhispererServiceTest { TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown()), projectRule.fixture.editor, projectRule.project, - file, - LatencyContext() + file ) assertThat(actual.supplementalContext).isNotNull @@ -182,12 +186,11 @@ class CodeWhispererServiceTest { fileContextInfo = mockFileContext, supplementalContextDeferred = async { mockSupContext }, connection = ToolkitConnectionManager.getInstance(projectRule.project).activeConnection(), - latencyContext = LatencyContext(), customizationArn = "fake-arn" ) ) - sut.invokeCodeWhispererInBackground(mockRequestContext).join() + sut.invokeCodeWhispererInBackground(mockRequestContext, 0, LatencyContext())?.join() verify(mockRequestContext, times(1)).awaitSupplementalContext() verify(clientFacade).generateCompletionsPaginator(any()) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt index bde3d169f52..004854730ad 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt @@ -4,7 +4,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer import com.intellij.analysis.problemsView.toolWindow.ProblemsView -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.wm.RegisterToolWindowTask import com.intellij.openapi.wm.ToolWindow @@ -18,14 +17,12 @@ import org.junit.Ignore import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.never -import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import software.aws.toolkits.jetbrains.core.ToolWindowHeadlessManagerImpl import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfiguration import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.services.codewhisperer.status.CodeWhispererStatusBarWidgetFactory @@ -34,14 +31,11 @@ import kotlin.test.fail class CodeWhispererSettingsTest : CodeWhispererTestBase() { - private lateinit var codewhispererServiceSpy: CodeWhispererService private lateinit var toolWindowHeadlessManager: ToolWindowHeadlessManagerImpl @Before override fun setUp() { super.setUp() - codewhispererServiceSpy = spy(codewhispererService) - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) // Create a mock ToolWindowManager with working implementation of setAvailable() and isAvailable() toolWindowHeadlessManager = object : ToolWindowHeadlessManagerImpl(projectRule.project) { @@ -83,7 +77,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { whenever(stateManager.checkActiveCodeWhispererConnectionType(projectRule.project)).thenReturn(CodeWhispererLoginType.Logout) assertThat(isCodeWhispererEnabled(projectRule.project)).isFalse invokeCodeWhispererService() - verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any()) + verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any()) } @Test @@ -92,7 +86,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { assertThat(stateManager.isAutoEnabled()).isFalse runInEdtAndWait { projectRule.fixture.type(':') - verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any()) + verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any()) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt index 0397576e8b6..93c93654008 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererStateTest.kt @@ -12,13 +12,16 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispe import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.telemetry.CodewhispererLanguage import software.aws.toolkits.telemetry.CodewhispererTriggerType +import kotlin.test.assertNotNull class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets request metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> - val actualRequestContext = states.requestContext + withCodeWhispererServiceInvokedAndWait { session -> + val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId + val actualRequestContext = codewhispererService.ongoingRequestsContext[selectedJobId] + assertNotNull(actualRequestContext) val editor = projectRule.fixture.editor val (actualProject, actualEditor, actualTriggerTypeInfo, actualCaretPosition, actualFileContextInfo) = actualRequestContext val (actualCaretContext, actualFilename, actualProgrammingLanguage) = actualFileContextInfo @@ -46,8 +49,10 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets response metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> - val actualResponseContext = states.responseContext + withCodeWhispererServiceInvokedAndWait { session -> + val selectedJobId = codewhispererService.getAllSuggestionsPreviewInfo()[session.selectedIndex].jobId + val actualResponseContext = codewhispererService.getAllPaginationSessions()[selectedJobId]?.responseContext + assertNotNull(actualResponseContext) assertThat(listOf(actualResponseContext.sessionId)).isEqualTo( pythonResponse.sdkHttpResponse().headers()[CodeWhispererService.KET_SESSION_ID] ) @@ -56,7 +61,9 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets recommendation metadata correctly`() { - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { + val states = codewhispererService.getAllPaginationSessions()[0] + assertNotNull(states) val actualRecommendationContext = states.recommendationContext val (actualDetailContexts, actualUserInput) = actualRecommendationContext @@ -74,15 +81,13 @@ class CodeWhispererStateTest : CodeWhispererTestBase() { @Test fun `test CodeWhisperer invocation sets initial typeahead and selected index correctly`() { - withCodeWhispererServiceInvokedAndWait { - val sessionContext = popupManagerSpy.sessionContext - val actualSelectedIndex = sessionContext.selectedIndex - val actualTypeahead = sessionContext.typeahead - val actualTypeaheadOriginal = sessionContext.typeaheadOriginal + withCodeWhispererServiceInvokedAndWait { session -> + val actualSelectedIndex = session.selectedIndex + val preview = codewhispererService.getAllSuggestionsPreviewInfo()[actualSelectedIndex] + val actualTypeahead = preview.typeahead assertThat(actualSelectedIndex).isEqualTo(0) assertThat(actualTypeahead).isEqualTo("") - assertThat(actualTypeaheadOriginal).isEqualTo("") } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt index 263d1f81c72..c61515cabe4 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryServiceTest.kt @@ -37,8 +37,10 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererCon import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager +import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus +import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService @@ -86,7 +88,7 @@ class CodeWhispererTelemetryServiceTest { mockClient = spy(CodeWhispererClientAdaptor.getInstance(projectRule.project)) mockClient.stub { onGeneric { - sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any()) + sendUserTriggerDecisionTelemetry(any(), any(), any(), any(), any(), any(), any(), any()) }.doAnswer { mock() } @@ -97,25 +99,20 @@ class CodeWhispererTelemetryServiceTest { @After fun cleanup() { sut.previousDecisions().clear() + CodeWhispererService.getInstance().disposeDisplaySession(false) + CodeWhispererService.getInstance().getAllPaginationSessions().clear() } @Test fun `test recordSuggestionState`() { fun assertSuggestionStates(expectedStates: List) { - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(expectedStates) + val recommendationContext = aRecommendationContext(expectedStates) val hasUserAccepted = expectedStates.any { it == CodewhispererSuggestionState.Accept } val details = recommendationContext.details val actualStates = mutableListOf() details.forEachIndexed { index, detail -> - val suggestionState = sut.recordSuggestionState( - index, - sessionContext.selectedIndex, - sessionContext.seen.contains(index), - hasUserAccepted, - detail.isDiscarded, - detail.recommendation.content().isEmpty() - ) + val suggestionState = sut.recordSuggestionState(detail, hasUserAccepted) actualStates.add(suggestionState) } @@ -156,7 +153,7 @@ class CodeWhispererTelemetryServiceTest { @Test fun `test aggregateUserDecision`() { - fun assertAggregateUserDecision(decisions: List, expected: CodewhispererPreviousSuggestionState) { + fun assertAggregateUserDecision(decisions: List, expected: CodewhispererSuggestionState) { val actual = sut.aggregateUserDecision(decisions) assertThat(actual).isEqualTo(expected) } @@ -169,7 +166,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Unseen, CodewhispererSuggestionState.Unseen ), - CodewhispererPreviousSuggestionState.Accept + CodewhispererSuggestionState.Accept ) assertAggregateUserDecision( @@ -180,7 +177,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Ignore ), - CodewhispererPreviousSuggestionState.Reject + CodewhispererSuggestionState.Reject ) assertAggregateUserDecision( @@ -191,7 +188,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Discard, CodewhispererSuggestionState.Empty ), - CodewhispererPreviousSuggestionState.Discard + CodewhispererSuggestionState.Discard ) assertAggregateUserDecision( @@ -201,7 +198,7 @@ class CodeWhispererTelemetryServiceTest { CodewhispererSuggestionState.Empty, CodewhispererSuggestionState.Empty ), - CodewhispererPreviousSuggestionState.Empty + CodewhispererSuggestionState.Empty ) } @@ -219,6 +216,7 @@ class CodeWhispererTelemetryServiceTest { ) val supplementalContextInfo = aSupplementalContextInfo() + val sessionContext = aSessionContext(projectRule.project) val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also { runTest { it.awaitSupplementalContext() } } @@ -235,6 +233,7 @@ class CodeWhispererTelemetryServiceTest { } sut.sendUserTriggerDecisionEvent( + sessionContext, requestContext, responseContext, recommendationContext, @@ -252,7 +251,7 @@ class CodeWhispererTelemetryServiceTest { "codewhisperer_userTriggerDecision", 1, "codewhispererSessionId" to responseContext.sessionId, - "codewhispererFirstRequestId" to requestContext.latencyContext.firstRequestId, + "codewhispererFirstRequestId" to sessionContext.latencyContext.firstRequestId, "codewhispererCompletionType" to recommendationContext.details[0].completionType, "codewhispererLanguage" to requestContext.fileContextInfo.programmingLanguage.toTelemetryType(), "codewhispererTriggerType" to requestContext.triggerTypeInfo.triggerType, @@ -281,21 +280,29 @@ class CodeWhispererTelemetryServiceTest { @Test fun `sendUserDecisionEventForAll will send userDecision event for all suggestions`() { - doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any()) + doNothing().whenever(sut).sendUserTriggerDecisionEvent(any(), any(), any(), any(), any(), any(), any(), any(), any()) val eventCount = mutableMapOf() var totalEventCount = 0 - val requestContext = aRequestContext(projectRule.project) - val responseContext = aResponseContext() fun assertUserDecision(decisions: List) { decisions.forEach { eventCount[it] = 1 + (eventCount[it] ?: 0) } totalEventCount += decisions.size - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(decisions) + CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext( + aRequestContext(projectRule.project), + aResponseContext(), + aRecommendationContext(decisions) + ) val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept } val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration) + val sessionContext = aSessionContext(projectRule.project) + sessionContext.selectedIndex = 0 + sut.sendUserDecisionEventForAll( + sessionContext, + hasUserAccept, + popupShownDuration + ) argumentCaptor().apply { verify(batcher, atLeastOnce()).enqueue(capture()) @@ -348,12 +355,21 @@ class CodeWhispererTelemetryServiceTest { val requestContext = aRequestContext(projectRule.project, mySupplementalContextInfo = supplementalContextInfo).also { runTest { it.awaitSupplementalContext() } } - val responseContext = aResponseContext() - val (recommendationContext, sessionContext) = aRecommendationContextAndSessionContext(decisions) val hasUserAccept = decisions.any { it == CodewhispererSuggestionState.Accept } val popupShownDuration = Duration.ofSeconds(Random.nextLong(0, 30)) - sut.sendUserDecisionEventForAll(requestContext, responseContext, recommendationContext, sessionContext, hasUserAccept, popupShownDuration) + CodeWhispererService.getInstance().getAllPaginationSessions()[0] = InvocationContext( + requestContext, + aResponseContext(), + aRecommendationContext(decisions) + ) + val sessionContext = aSessionContext(projectRule.project) + sessionContext.selectedIndex = 0 + sut.sendUserDecisionEventForAll( + sessionContext, + hasUserAccept, + popupShownDuration + ) argumentCaptor().apply { verify(batcher, atLeastOnce()).enqueue(capture()) @@ -439,6 +455,7 @@ class CodeWhispererTelemetryServiceTest { ) AwsSettings.getInstance().isTelemetryEnabled = isTelemetryEnabled + val expectedSessionContext = aSessionContext(projectRule.project) val expectedRequestContext = aRequestContext(projectRule.project) val expectedResponseContext = aResponseContext() val expectedRecommendationContext = aRecommendationContext() @@ -449,6 +466,7 @@ class CodeWhispererTelemetryServiceTest { val expectedCharCount = 100 val expectedCompletionType = expectedRecommendationContext.details[0].completionType sut.sendUserTriggerDecisionEvent( + expectedSessionContext, expectedRequestContext, expectedResponseContext, expectedRecommendationContext, @@ -461,6 +479,7 @@ class CodeWhispererTelemetryServiceTest { if (isProTier || isTelemetryEnabled) { verify(mockClient).sendUserTriggerDecisionTelemetry( + eq(expectedSessionContext), eq(expectedRequestContext), eq(expectedResponseContext), eq(expectedCompletionType), diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt index 028008ce55f..ddd23bf52dc 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Editor -import com.intellij.openapi.ui.popup.JBPopup import com.intellij.psi.PsiDocumentManager import com.intellij.testFramework.TestActionEvent import com.intellij.testFramework.replaceService @@ -58,6 +57,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.R import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.AcceptedSuggestionEntry import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker @@ -73,6 +74,7 @@ import software.aws.toolkits.telemetry.CodewhispererSuggestionState import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Result import java.time.Instant +import kotlin.test.assertNotNull class CodeWhispererTelemetryTest : CodeWhispererTestBase() { private val userDecision = "codewhisperer_userDecision" @@ -105,12 +107,10 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { @Test fun `test pre-setup failure will send service invocation event with failed status`() { - val codewhispererServiceSpy = spy(codewhispererService) { - onGeneric { getRequestContext(any(), any(), any(), any(), any()) } + codewhispererService.stub { + onGeneric { getRequestContext(any(), any(), any(), any()) } .doAnswer { throw Exception() } } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - invokeCodeWhispererService() argumentCaptor().apply { @@ -189,7 +189,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { @Test fun `test cancelling popup will send user decision event for all unseen but one rejected`() { withCodeWhispererServiceInvokedAndWait { states -> - popupManagerSpy.cancelPopup(states.popup) + codewhispererService.disposeDisplaySession(false) val count = pythonResponse.completions().size argumentCaptor().apply { @@ -373,68 +373,45 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { @Test fun `test invoking CodeWhisperer will send service invocation event with sessionId and requestId from response`() { - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { session -> val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) + val detail = codewhispererService.getAllSuggestionsPreviewInfo()[0].detail + val states = codewhispererService.getAllPaginationSessions()[0] + assertNotNull(states) assertEventsContainsFieldsAndCount( metricCaptor.allValues, serviceInvocation, 1, "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, + "codewhispererRequestId" to detail.requestId, ) } } @Test fun `test userDecision events will record sessionId and requestId from response`() { - val statesCaptor = argumentCaptor() - withCodeWhispererServiceInvokedAndWait {} - verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(statesCaptor.capture(), any(), any(), any(), any()) - val states = statesCaptor.lastValue + val sessionCaptor = argumentCaptor() + var states: InvocationContext? = null + var previews: List? = null + withCodeWhispererServiceInvokedAndWait { + states = codewhispererService.getAllPaginationSessions()[0] + previews = codewhispererService.getAllSuggestionsPreviewInfo() + } + verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(sessionCaptor.capture(), any(), any()) val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) + assertNotNull(states) + assertNotNull(previews) assertEventsContainsFieldsAndCount( metricCaptor.allValues, userDecision, - states.recommendationContext.details.size, - "codewhispererSessionId" to states.responseContext.sessionId, - "codewhispererRequestId" to states.recommendationContext.details[0].requestId, + previews?.size ?: 0, + "codewhispererSessionId" to states?.responseContext?.sessionId, + "codewhispererRequestId" to previews?.get(0)?.detail?.requestId, ) } - @Test - fun `test showing IntelliSense after triggering CodeWhisperer will send userDecision events of state Discard`() { - val codewhispererServiceSpy = spy(codewhispererService) - codewhispererServiceSpy.stub { - onGeneric { - canDoInvocation(any(), any()) - } doAnswer { - true - } - } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) - popupManagerSpy.stub { - onGeneric { - hasConflictingPopups(any()) - } doAnswer { - true - } - } - invokeCodeWhispererService() - - runInEdtAndWait { - val metricCaptor = argumentCaptor() - verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) - assertEventsContainsFieldsAndCount( - metricCaptor.allValues, - userDecision, - pythonResponse.completions().size, - codewhispererSuggestionState to CodewhispererSuggestionState.Discard.toString(), - ) - } - } - @Test fun `test codePercentage tracker will not be activated if CWSPR terms of service is not accepted`() { val exploreManagerMock = mock { @@ -672,7 +649,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { } invokeCodeWhispererService() - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, never()).showPopup(any(), any()) runInEdtAndWait { val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture()) @@ -693,7 +670,9 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { } doReturnConsecutively(listOf(pythonResponseWithNonEmptyToken, emptyListResponse)) } - withCodeWhispererServiceInvokedAndWait { } + withCodeWhispererServiceInvokedAndWait { + popupManagerSpy.popupComponents.acceptButton.doClick() + } runInEdtAndWait { val metricCaptor = argumentCaptor() @@ -772,13 +751,12 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { val numOfEmptyRecommendations = response.completions().filter { it.content().isEmpty() }.size if (numOfEmptyRecommendations == response.completions().size) { - verify(popupManagerSpy, never()).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, never()).showPopup(any(), any()) } else { - val popupCaptor = argumentCaptor() verify(popupManagerSpy, timeout(5000)) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + .showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } runInEdtAndWait { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt index 6b76b28c939..02047fb6605 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt @@ -19,7 +19,6 @@ import org.junit.Rule import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doNothing import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.timeout @@ -41,7 +40,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhisper import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreStateType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager @@ -97,9 +96,11 @@ open class CodeWhispererTestBase { } } + codewhispererService = spy(CodeWhispererService.getInstance()) + ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererService, disposableRule.disposable) + popupManagerSpy = spy(CodeWhispererPopupManager.getInstance()) - popupManagerSpy.reset() - doNothing().`when`(popupManagerSpy).showPopup(any(), any(), any(), any(), any()) + codewhispererService.disposeDisplaySession(false) ApplicationManager.getApplication().replaceService(CodeWhispererPopupManager::class.java, popupManagerSpy, disposableRule.disposable) invocationStatusSpy = spy(CodeWhispererInvocationStatus.getInstance()) @@ -114,7 +115,6 @@ open class CodeWhispererTestBase { stateManager = spy(CodeWhispererExplorerActionManager.getInstance()) recommendationManager = CodeWhispererRecommendationManager.getInstance() - codewhispererService = CodeWhispererService.getInstance() editorManager = CodeWhispererEditorManager.getInstance() settingsManager = CodeWhispererSettings.getInstance() @@ -153,24 +153,22 @@ open class CodeWhispererTestBase { open fun tearDown() { stateManager.loadState(originalExplorerActionState) settingsManager.loadState(originalSettings) - popupManagerSpy.reset() - runInEdtAndWait { - popupManagerSpy.closePopup() - } + codewhispererService.disposeDisplaySession(true) } - fun withCodeWhispererServiceInvokedAndWait(runnable: (InvocationContext) -> Unit) { - val statesCaptor = argumentCaptor() + fun withCodeWhispererServiceInvokedAndWait(runnable: (SessionContext) -> Unit) { + val sessionCaptor = argumentCaptor() invokeCodeWhispererService() verify(popupManagerSpy, timeout(5000).atLeastOnce()) - .showPopup(statesCaptor.capture(), any(), any(), any(), any()) - val states = statesCaptor.lastValue + .showPopup(sessionCaptor.capture(), any()) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) + val session = sessionCaptor.lastValue runInEdtAndWait { try { - runnable(states) + runnable(session) } finally { - CodeWhispererPopupManager.getInstance().closePopup(states.popup) + codewhispererService.disposeDisplaySession(true) } } @@ -194,7 +192,7 @@ open class CodeWhispererTestBase { jobRef.get()?.join() // wait for subsequent background operations to be complete - while (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) { + while (CodeWhispererInvocationStatus.getInstance().hasExistingServiceInvocation()) { yield() delay(10) } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt index 06db93b345c..50cc14009fe 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTypeaheadTest.kt @@ -34,58 +34,14 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() { testTypingTypeaheadMatchingRecommendationShouldMatchRightContext(testRightContext) } - @Test - fun `test typing blank typeahead should correctly update typeahead state`() { - val testTypeaheadOriginal = " " - testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly(testTypeaheadOriginal, 5, 1) - } - - @Test - fun `test typing typeahead with leading spaces and matching suffix should correctly update typeahead state`() { - val testTypeaheadOriginal = " test" - testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly(testTypeaheadOriginal, 3, 3) - } - - private fun testTypingTypeaheadWithLeadingSpaceShouldMatchTypeaheadStateCorrectly( - expectedTypeaheadOriginal: String, - expectedNumOfValidRecommendation: Int, - expectedSelectedAfterBackspace: Int - ) { - withCodeWhispererServiceInvokedAndWait { states -> - val editor = projectRule.fixture.editor - val startOffset = editor.caretModel.offset - expectedTypeaheadOriginal.forEach { char -> - projectRule.fixture.type(char) - val caretOffset = editor.caretModel.offset - val actualTypeaheadOriginal = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - val actualTypeahead = actualTypeaheadOriginal.trimStart() - assertThat(popupManagerSpy.sessionContext.typeaheadOriginal).isEqualTo(actualTypeaheadOriginal) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(actualTypeahead) - assertThat(states.popup.isDisposed).isFalse - } - checkRecommendationInfoLabelText(1, expectedNumOfValidRecommendation) - - // Backspacing for the same amount of times - expectedTypeaheadOriginal.forEach { _ -> - projectRule.fixture.type('\b') - val caretOffset = editor.caretModel.offset - val actualTypeaheadOriginal = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - val actualTypeahead = actualTypeaheadOriginal.trimStart() - assertThat(popupManagerSpy.sessionContext.typeaheadOriginal).isEqualTo(actualTypeaheadOriginal) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(actualTypeahead) - assertThat(states.popup.isDisposed).isFalse - } - checkRecommendationInfoLabelText(expectedSelectedAfterBackspace, 5) - } - } - private fun testTypingTypeaheadMatchingRecommendationShouldMatchRightContext(rightContext: String) { projectRule.fixture.configureByText(pythonFileName, pythonTestLeftContext + rightContext) runInEdtAndWait { projectRule.fixture.editor.caretModel.moveToOffset(pythonTestLeftContext.length) } - withCodeWhispererServiceInvokedAndWait { states -> - val recommendation = states.recommendationContext.details[0].reformatted.content() + withCodeWhispererServiceInvokedAndWait { session -> + var preview = codewhispererService.getAllSuggestionsPreviewInfo()[0] + val recommendation = preview.detail.reformatted.content() val editor = projectRule.fixture.editor val startOffset = editor.caretModel.offset recommendation.forEachIndexed { index, char -> @@ -93,7 +49,8 @@ class CodeWhispererTypeaheadTest : CodeWhispererTestBase() { projectRule.fixture.type(char) val caretOffset = editor.caretModel.offset val typeahead = editor.document.charsSequence.subSequence(startOffset, caretOffset).toString() - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead) + preview = codewhispererService.getAllSuggestionsPreviewInfo()[0] + assertThat(preview.typeahead).isEqualTo(typeahead) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt index d46a603e61b..c08238b7a17 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserActionsTest.kt @@ -14,14 +14,12 @@ import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TEXT_END_WITH_ import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TEXT_START_WITH_SELECTION import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.event.VisibleAreaEvent -import com.intellij.openapi.ui.popup.JBPopup import com.intellij.testFramework.runInEdtAndWait import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mockito.times import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.timeout @@ -95,7 +93,7 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { } withCodeWhispererServiceInvokedAndWait { projectRule.fixture.performEditorAction(actionId) - verify(popupManagerSpy, timeout(5000)).cancelPopup(any()) + verify(codewhispererService, timeout(5000).atLeastOnce()).disposeDisplaySession(false) } } @@ -120,11 +118,9 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { projectRule.fixture.type('\n') val expectedFileContext = "$testLeftContext\n \n $testRightContext" assertThat(projectRule.fixture.editor.document.text).isEqualTo(expectedFileContext) - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000)) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + verify(popupManagerSpy, timeout(5000)).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } @@ -138,10 +134,10 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { on { this.newRectangle } doReturn newRect } withCodeWhispererServiceInvokedAndWait { states -> - CodeWhispererInvocationStatus.getInstance().setPopupActive(true) + CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) val listener = CodeWhispererScrollListener(states) listener.visibleAreaChanged(event) - verify(popupManagerSpy, times(2)).showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, times(2)).showPopup(any(), any()) } } @@ -163,15 +159,12 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { setFileContext(pythonFileName, "def", rightContext) projectRule.fixture.type('{') if (shouldtrigger) { - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000).atLeastOnce()) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + verify(popupManagerSpy, timeout(5000).atLeastOnce()).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } else { - verify(popupManagerSpy, times(0)) - .showPopup(any(), any(), any(), any(), any()) + verify(popupManagerSpy, times(0)).showPopup(any(), any()) } } @@ -179,11 +172,9 @@ class CodeWhispererUserActionsTest : CodeWhispererTestBase() { CodeWhispererExplorerActionManager.getInstance().setAutoEnabled(true) setFileContext(pythonFileName, prompt, "") projectRule.fixture.type('\n') - val popupCaptor = argumentCaptor() - verify(popupManagerSpy, timeout(5000).atLeast(times)) - .showPopup(any(), any(), popupCaptor.capture(), any(), any()) + verify(popupManagerSpy, timeout(5000).atLeast(times)).showPopup(any(), any()) runInEdtAndWait { - popupManagerSpy.closePopup(popupCaptor.lastValue) + codewhispererService.disposeDisplaySession(true) } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt index 8c4c562481e..f534eeb37b6 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererUserInputTest.kt @@ -3,21 +3,16 @@ package software.aws.toolkits.jetbrains.services.codewhisperer -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile -import com.intellij.testFramework.replaceService import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.spy import org.mockito.kotlin.stub import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse -import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService class CodeWhispererUserInputTest : CodeWhispererTestBase() { @@ -25,10 +20,8 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { fun `test no user input should show all recommendations`() { addUserInputAfterInvocation("") - withCodeWhispererServiceInvokedAndWait { states -> - val actualRecommendations = states.recommendationContext.details.map { - it.recommendation.content() - } + withCodeWhispererServiceInvokedAndWait { session -> + val actualRecommendations = codewhispererService.getAllSuggestionsPreviewInfo().map { it.detail.recommendation.content() } assertThat(actualRecommendations).isEqualTo(pythonResponse.completions().map { it.content() }) } } @@ -40,10 +33,11 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { val expectedRecommendations = pythonResponse.completions().map { it.content() } - withCodeWhispererServiceInvokedAndWait { states -> - val actualRecommendations = states.recommendationContext.details.map { it.recommendation.content() } + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + val actualRecommendations = previews.map { it.detail.recommendation.content() } assertThat(actualRecommendations).isEqualTo(expectedRecommendations) - states.recommendationContext.details.forEachIndexed { index, context -> + previews.map { it.detail }.forEachIndexed { index, context -> val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) val actualDiscarded = context.isDiscarded assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -58,10 +52,11 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { val typeahead = " recommendation" - withCodeWhispererServiceInvokedAndWait { states -> + withCodeWhispererServiceInvokedAndWait { session -> projectRule.fixture.type(typeahead) - assertThat(popupManagerSpy.sessionContext.typeahead).isEqualTo(typeahead) - states.recommendationContext.details.forEachIndexed { index, actualContext -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].typeahead).isEqualTo(typeahead) + previews.map { it.detail }.forEachIndexed { index, actualContext -> val actualDiscarded = actualContext.isDiscarded val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput + typeahead) assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -75,9 +70,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { addUserInputAfterInvocation(blankUserInput) val userInput = blankUserInput.trimStart() - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { _, actualContext -> + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput) + previews.map { it.detail }.forEachIndexed { _, actualContext -> assertThat(actualContext.isDiscarded).isEqualTo(false) } } @@ -89,9 +85,10 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { addUserInputAfterInvocation(userInputWithLeadingSpaces) val userInput = userInputWithLeadingSpaces.trimStart() - withCodeWhispererServiceInvokedAndWait { states -> - assertThat(states.recommendationContext.userInputSinceInvocation).isEqualTo(userInput) - states.recommendationContext.details.forEachIndexed { index, actualContext -> + withCodeWhispererServiceInvokedAndWait { session -> + val previews = codewhispererService.getAllSuggestionsPreviewInfo() + assertThat(previews[session.selectedIndex].userInput).isEqualTo(userInput) + previews.map { it.detail }.forEachIndexed { index, actualContext -> val actualDiscarded = actualContext.isDiscarded val expectedDiscarded = !pythonResponse.completions()[index].content().startsWith(userInput) assertThat(actualDiscarded).isEqualTo(expectedDiscarded) @@ -100,33 +97,28 @@ class CodeWhispererUserInputTest : CodeWhispererTestBase() { } private fun addUserInputAfterInvocation(userInput: String) { - val codewhispererServiceSpy = spy(codewhispererService) val triggerTypeCaptor = argumentCaptor() val editorCaptor = argumentCaptor() val projectCaptor = argumentCaptor() val psiFileCaptor = argumentCaptor() - val latencyContextCaptor = argumentCaptor() - codewhispererServiceSpy.stub { + codewhispererService.stub { onGeneric { getRequestContext( triggerTypeCaptor.capture(), editorCaptor.capture(), projectCaptor.capture(), - psiFileCaptor.capture(), - latencyContextCaptor.capture() + psiFileCaptor.capture() ) }.doAnswer { - val requestContext = codewhispererServiceSpy.getRequestContext( + val requestContext = codewhispererService.getRequestContext( triggerTypeCaptor.firstValue, editorCaptor.firstValue, projectCaptor.firstValue, psiFileCaptor.firstValue, - latencyContextCaptor.firstValue ) projectRule.fixture.type(userInput) requestContext }.thenCallRealMethod() } - ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt index 5dcbaaacc6f..85ff06206dd 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.project.Project import kotlinx.coroutines.async @@ -69,6 +70,9 @@ object CodeWhispererTestUtil { const val codeWhispererCodeScanActionId = "codewhisperer.toolbar.security.scan" const val testValidAccessToken = "test_valid_access_token" const val testNextToken = "test_next_token" + const val ACTION_KEY_ACCEPT = "codewhisperer.inline.accept" + const val ACTION_KEY_NAV_PREV = "codewhisperer.inline.navigate.previous" + const val ACTION_KEY_NAV_NEXT = "codewhisperer.inline.navigate.next" private val testReferenceInfoPair = listOf( Pair("MIT", "testRepo1"), Pair("Apache-2.0", "testRepo2"), @@ -211,6 +215,12 @@ object CodeWhispererTestUtil { .build() } +fun aSessionContext( + project: Project = mock(), + editor: Editor = mock(), + latencyContext: LatencyContext = LatencyContext() +) = SessionContext(project, editor, latencyContext = latencyContext) + fun aRequestContext( project: Project, myFileContextInfo: FileContextInfo? = null, @@ -242,20 +252,6 @@ fun aRequestContext( fileContextInfo = myFileContextInfo ?: aFileContextInfo(), supplementalContextDeferred = supplementalContextDeferred, null, - LatencyContext( - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextDouble(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - Random.nextLong(), - aString() - ), customizationArn = null ) } @@ -306,14 +302,15 @@ fun aRecommendationContext(): RecommendationContext { details, aString(), aString(), - VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)) + VisualPosition(Random.nextInt(1, 100), Random.nextInt(1, 100)), + 0 ) } /** * util to generate a RecommendationContext and a SessionContext given expected decisions */ -fun aRecommendationContextAndSessionContext(decisions: List): Pair { +fun aRecommendationContext(decisions: List): RecommendationContext { val table = CodewhispererSuggestionState.values().associateWith { 0 }.toMutableMap() decisions.forEach { table[it]?.let { curCount -> table[it] = 1 + curCount } @@ -331,6 +328,8 @@ fun aRecommendationContextAndSessionContext(decisions: List() - decisions.forEachIndexed { index, decision -> - if (decision != CodewhispererSuggestionState.Unseen) { - seen.add(index) - } - } - - val sessionContext = SessionContext( - selectedIndex = selectedIndex, - seen = seen - ) - return recommendationContext to sessionContext + return recommendationContext } fun aCompletion(content: String? = null, isEmpty: Boolean = false, referenceCount: Int? = null, importCount: Int? = null): Completion { diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt index 80dea49e17c..9e25cfdf542 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/plugin/PluginUpdateManager.kt @@ -38,14 +38,16 @@ import software.aws.toolkits.telemetry.ToolkitTelemetry class PluginUpdateManager : Disposable { private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this) + fun isBeta() = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.descriptor?.version?.contains("beta") ?: false + fun scheduleAutoUpdate() { if (alarm.isDisposed) return scheduleUpdateTask() val enabled = AwsSettings.getInstance().isAutoUpdateEnabled - LOG.debug { "AWS plugins checking for new updates. Auto update enabled: $enabled" } + LOG.debug { "AWS plugins checking for new updates. Auto update enabled: $enabled, isBeta: ${isBeta()}" } - if (!enabled) return + if (!enabled && !isBeta()) return runInEdt { ProgressManager.getInstance().run(object : Task.Backgroundable( diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 5685f0f452c..34462ecefbd 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -767,6 +767,8 @@ codemodernizer.toolwindow.transformation.progress.running_time=Running time: {0} codewhisperer.actions.connect_github.title=Connect with Us on GitHub codewhisperer.actions.open_settings.title=Open Settings codewhisperer.actions.send_feedback.title=Send Feedback +codewhisperer.actions.switch_to_marketplace.progress.title=Switching to marketplace version +codewhisperer.actions.switch_to_marketplace.title=Switch Back to Marketplace codewhisperer.actions.view_documentation.title=View Documentation codewhisperer.codescan.apply_fix_button_label=Apply fix codewhisperer.codescan.apply_fix_button_tooltip=Apply suggested fix @@ -879,13 +881,16 @@ codewhisperer.notification.custom.not_available=Selected Amazon Q customization codewhisperer.notification.custom.simple.button.got_it=Got it codewhisperer.notification.custom.simple.button.select_another_customization=Select another customization codewhisperer.notification.custom.simple.button.select_customization=Select customization +codewhisperer.notification.inline.shortcut_config.content=Check out the default and customize in the keymap settings. +codewhisperer.notification.inline.shortcut_config.open_setting=Open keymap settings +codewhisperer.notification.inline.shortcut_config.title=Configurable Amazon Q suggestion shortcuts codewhisperer.notification.remote.ide_unsupported.message=Please update your IDE backend to a 2023.3 or later version to continue using Amazon Q inline suggestions. codewhisperer.notification.remote.ide_unsupported.title=Amazon Q inline suggestion not supported in this IDE version codewhisperer.notification.usage_limit.codescan.warn.content=Amazon Q: You have reached the monthly limit for project scans. codewhisperer.notification.usage_limit.codesuggestion.warn.content=You have reached the monthly fair use limit of code recommendations. codewhisperer.popup.button.accept=
 Insert Code 
\u21E5
-codewhisperer.popup.button.next=
Next
-codewhisperer.popup.button.prev=
Previous
+codewhisperer.popup.button.next=
Next
{1}
+codewhisperer.popup.button.prev=
Previous
{1}
codewhisperer.popup.import_info=

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

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