From f20301f6edcbb5ee9f3a057497aa62550ed0bb0a Mon Sep 17 00:00:00 2001 From: Andrew Yu Date: Tue, 4 Mar 2025 14:35:59 -0800 Subject: [PATCH 1/2] fix(amazonq): Revert popup position and visibility change now popup goes back to the behavior of being displayed below the suggestions and is always visible --- ...-5b5b9cf3-701c-48b5-b232-96e726a9a860.json | 4 + .../popup/CodeWhispererPopupManager.kt | 96 +++++++------------ .../popup/CodeWhispererUIChangeListener.kt | 26 +++++ ...hispererPopupIntelliSenseAcceptListener.kt | 60 ------------ .../service/CodeWhispererService.kt | 17 +--- 5 files changed, 68 insertions(+), 135 deletions(-) create mode 100644 .changes/next-release/removal-5b5b9cf3-701c-48b5-b232-96e726a9a860.json delete mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPopupIntelliSenseAcceptListener.kt diff --git a/.changes/next-release/removal-5b5b9cf3-701c-48b5-b232-96e726a9a860.json b/.changes/next-release/removal-5b5b9cf3-701c-48b5-b232-96e726a9a860.json new file mode 100644 index 00000000000..3bd03cf4d9e --- /dev/null +++ b/.changes/next-release/removal-5b5b9cf3-701c-48b5-b232-96e726a9a860.json @@ -0,0 +1,4 @@ +{ + "type" : "removal", + "description" : "The Amazon Q inline suggestion popup goes back to being under the suggestions and is always showing." +} \ No newline at end of file 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 d864fe5a5d6..234813002bb 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 @@ -5,7 +5,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup import com.intellij.codeInsight.hint.ParameterInfoController import com.intellij.codeInsight.lookup.LookupManager -import com.intellij.codeInsight.lookup.LookupManagerListener import com.intellij.idea.AppMode import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ESCAPE @@ -26,8 +25,6 @@ import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener -import com.intellij.openapi.editor.event.EditorMouseEvent -import com.intellij.openapi.editor.event.EditorMouseMotionListener import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener import com.intellij.openapi.fileEditor.FileEditorManager @@ -65,10 +62,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.Cod import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererAcceptButtonActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererNextButtonActionListener -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererPopupIntelliSenseAcceptListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererPrevButtonActionListener import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.addIntelliSenseAcceptListener import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager @@ -224,6 +219,7 @@ class CodeWhispererPopupManager { fun render( states: InvocationContext, sessionContext: SessionContext, + overlappingLinesCount: Int, isRecommendationAdded: Boolean, ) { updatePopupPanel(states, sessionContext) @@ -246,7 +242,7 @@ class CodeWhispererPopupManager { states.requestContext.latencyContext.getPerceivedLatency(states.requestContext.triggerTypeInfo.triggerType) } if (!isRecommendationAdded) { - showPopup(states, sessionContext, states.popup, visible = sessionContext.isPopupShowing) + showPopup(states, sessionContext, states.popup, overlappingLinesCount) } } @@ -285,30 +281,28 @@ class CodeWhispererPopupManager { states: InvocationContext, sessionContext: SessionContext, popup: JBPopup, - visible: Boolean = false, + overlappingLinesCount: Int, ) { val caretPoint = states.requestContext.editor.offsetToXY(states.requestContext.caretPosition.offset) val editor = states.requestContext.editor val detailContexts = states.recommendationContext.details val userInputOriginal = states.recommendationContext.userInputOriginal + val userInput = states.recommendationContext.userInputSinceInvocation val selectedIndex = sessionContext.selectedIndex val typeaheadOriginal = sessionContext.typeaheadOriginal val typeahead = sessionContext.typeahead val userInputLines = userInputOriginal.split("\n").size - 1 + val lineCount = getReformattedRecommendation(detailContexts[selectedIndex], userInput).split("\n").size val additionalLines = typeaheadOriginal.split("\n").size - typeahead.split("\n").size val popupSize = (popup as AbstractPopup).preferredContentSize + val yBelowLastLine = caretPoint.y + (lineCount + additionalLines + userInputLines - overlappingLinesCount) * editor.lineHeight val yAboveFirstLine = caretPoint.y - popupSize.height + (additionalLines + userInputLines) * editor.lineHeight val editorRect = editor.scrollingModel.visibleArea - val popupRect = Rectangle(caretPoint.x, yAboveFirstLine, popupSize.width, popupSize.height) + var popupRect = Rectangle(caretPoint.x, yBelowLastLine, popupSize.width, popupSize.height) var noEnoughSpaceForPopup = false CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(true) - if (!editorRect.contains(popupRect)) { - // popup location above first line don't work, so don't show the popup - noEnoughSpaceForPopup = true - } - // Check if the current editor still has focus. If not, don't show the popup. val isSameEditorAsTrigger = if (!AppMode.isRemoteDevHost()) { editor.contentComponent.isFocusOwner @@ -321,8 +315,25 @@ class CodeWhispererPopupManager { return } - // popup to always display above the current editing line - val popupLocation = Point(caretPoint.x, yAboveFirstLine) + val popupLocation = + if (!editorRect.contains(popupRect)) { + popupRect = Rectangle(caretPoint.x, yAboveFirstLine, popupSize.width, popupSize.height) + if (!editorRect.contains(popupRect)) { + // both popup location (below last line and above first line) don't work, so don't show the popup + noEnoughSpaceForPopup = true + } + LOG.debug { + "Show popup above the first line of recommendation. " + + "Editor position: $editorRect, popup position: $popupRect" + } + Point(caretPoint.x, yAboveFirstLine) + } else { + LOG.debug { + "Show popup below the last line of recommendation. " + + "Editor position: $editorRect, popup position: $popupRect" + } + Point(caretPoint.x, yBelowLastLine) + } val relativePopupLocationToEditor = RelativePoint(editor.contentComponent, popupLocation) @@ -335,12 +346,9 @@ class CodeWhispererPopupManager { } } else { if (!AppMode.isRemoteDevHost()) { - if (visible && !noEnoughSpaceForPopup) { - // TODO: will move to a keybinding listener once I found one - popupComponents.prevButton.text = popupComponents.prevButtonText() - popupComponents.nextButton.text = popupComponents.nextButtonText() - popup.show(relativePopupLocationToEditor) - } + popupComponents.prevButton.text = popupComponents.prevButtonText() + popupComponents.nextButton.text = popupComponents.nextButtonText() + popup.show(relativePopupLocationToEditor) } else { // TODO: For now, the popup will always display below the suggestions, without checking // if the location the popup is about to show at stays in the editor window or not, due to @@ -369,27 +377,14 @@ class CodeWhispererPopupManager { } } - bringSuggestionInlayToFront(editor, popup, sessionContext, !visible) - } - - // opposite == false: show Q, hide IntelliSense - // opposite == true: show IntelliSense, hide Q - fun bringSuggestionInlayToFront( - editor: Editor, - popup: JBPopup?, - sessionContext: SessionContext, - opposite: Boolean = false, - ) { - val qInlinePopupAlpha = if (opposite) 1f else 0.1f - val intelliSensePopupAlpha = if (opposite) 0f else 0.8f - - (popup as AbstractPopup?)?.popupWindow?.let { - WindowManager.getInstance().setAlphaModeRatio(it, qInlinePopupAlpha) - } - ComponentUtil.getWindow(LookupManager.getActiveLookup(editor)?.component)?.let { - WindowManager.getInstance().setAlphaModeRatio(it, intelliSensePopupAlpha) + // popup.popupWindow is null in remote host + if (!AppMode.isRemoteDevHost()) { + if (noEnoughSpaceForPopup) { + WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 1f) + } else { + WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 0.1f) + } } - sessionContext.isPopupShowing = !opposite } fun initPopup(): JBPopup = JBPopupFactory.getInstance() @@ -458,13 +453,6 @@ class CodeWhispererPopupManager { } } ) - states.requestContext.project.messageBus.connect(states).subscribe( - LookupManagerListener.TOPIC, - CodeWhispererPopupIntelliSenseAcceptListener(states) - ) - LookupManager.getActiveLookup(states.requestContext.editor)?.let { - addIntelliSenseAcceptListener(it, states) - } } private fun addButtonActionListeners(states: InvocationContext) { @@ -556,18 +544,6 @@ class CodeWhispererPopupManager { window?.addComponentListener(windowListener) Disposer.register(states) { window?.removeComponentListener(windowListener) } } - - val suggestionHoverEnterListener: EditorMouseMotionListener = object : EditorMouseMotionListener { - override fun mouseMoved(e: EditorMouseEvent) { - if (e.inlay != null) { - showPopup(states, sessionContext, states.popup, visible = true) - } else { - bringSuggestionInlayToFront(editor, states.popup, sessionContext, opposite = true) - } - super.mouseMoved(e) - } - } - editor.addEditorMouseMotionListener(suggestionHoverEnterListener, states) } private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt index 6de006145a6..add8e835f02 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererUIChangeListener.kt @@ -91,14 +91,39 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { CodeWhispererPopupManager.getInstance().render( states, sessionContext, + overlappingLinesCount, isRecommendationAdded = false ) } override fun scrolled(states: InvocationContext, sessionContext: SessionContext) { + if (states.popup.isDisposed) return + val editor = states.requestContext.editor + val editorManager = CodeWhispererEditorManager.getInstance() + val selectedIndex = sessionContext.selectedIndex + val typeahead = sessionContext.typeahead + val detail = states.recommendationContext.details[selectedIndex] + + // get matching brackets from recommendations to the brackets after caret position + val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation( + detail, + states.recommendationContext.userInputSinceInvocation + ).substring(typeahead.length) + + val remainingLines = remaining.split("\n") + val otherLinesOfRemaining = remainingLines.drop(1) + + // process other lines inlays, where we do tail-head matching as much as possible + val overlappingLinesCount = editorManager.findOverLappingLines( + editor, + otherLinesOfRemaining, + detail.isTruncatedOnRight, + sessionContext + ) CodeWhispererPopupManager.getInstance().render( states, sessionContext, + overlappingLinesCount, isRecommendationAdded = false ) } @@ -107,6 +132,7 @@ class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener { CodeWhispererPopupManager.getInstance().render( states, sessionContext, + 0, isRecommendationAdded = true ) } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPopupIntelliSenseAcceptListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPopupIntelliSenseAcceptListener.kt deleted file mode 100644 index b243c95b253..00000000000 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/listeners/CodeWhispererPopupIntelliSenseAcceptListener.kt +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners - -import com.intellij.codeInsight.lookup.Lookup -import com.intellij.codeInsight.lookup.LookupEvent -import com.intellij.codeInsight.lookup.LookupListener -import com.intellij.codeInsight.lookup.LookupManagerListener -import com.intellij.codeInsight.lookup.impl.LookupImpl -import com.intellij.openapi.util.Disposer -import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager -import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus - -class CodeWhispererPopupIntelliSenseAcceptListener(private val states: InvocationContext) : LookupManagerListener { - override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) { - if (oldLookup != null || newLookup == null) return - - addIntelliSenseAcceptListener(newLookup, states) - } - - companion object { - val LOG = getLogger() - } -} - -fun addIntelliSenseAcceptListener(lookup: Lookup, states: InvocationContext) { - CodeWhispererPopupManager.getInstance().allowIntelliSenseDuringSuggestionPreview = true - val listener = object : LookupListener { - override fun itemSelected(event: LookupEvent) { - if (!CodeWhispererInvocationStatus.getInstance().isDisplaySessionActive() || - !(event.lookup as LookupImpl).isShown - ) { - cleanup() - return - } - CodeWhispererPopupManager.getInstance().changeStates( - states, - 0 - ) - cleanup() - } - - override fun lookupCanceled(event: LookupEvent) { - cleanup() - } - - private fun cleanup() { - lookup.removeLookupListener(this) - CodeWhispererPopupManager.getInstance().allowIntelliSenseDuringSuggestionPreview = false - } - } - lookup.addLookupListener(listener) - Disposer.register(states) { - lookup.removeLookupListener(listener) - CodeWhispererPopupManager.getInstance().allowIntelliSenseDuringSuggestionPreview = false - } -} 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 4f500ea14a4..faad851920d 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 @@ -699,7 +699,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { recommendationContext: RecommendationContext, popup: JBPopup, ): InvocationContext { - addPopupChildDisposables(requestContext.project, requestContext.editor, popup) + addPopupChildDisposables(popup) // Creating a disposable for managing all listeners lifecycle attached to the popup. // previously(before pagination) we use popup as the parent disposable. // After pagination, listeners need to be updated as states are updated, for the same popup, @@ -711,25 +711,12 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { return states } - private fun addPopupChildDisposables(project: Project, editor: Editor, popup: JBPopup) { + private fun addPopupChildDisposables(popup: JBPopup) { codeInsightSettingsFacade.disableCodeInsightUntil(popup) Disposer.register(popup) { CodeWhispererPopupManager.getInstance().reset() } - project.messageBus.connect(popup).subscribe( - CodeWhispererServiceNew.CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER, - object : CodeWhispererIntelliSenseOnHoverListener { - override fun onEnter() { - CodeWhispererPopupManager.getInstance().bringSuggestionInlayToFront( - editor, - popup, - CodeWhispererPopupManager.getInstance().sessionContext, - opposite = true - ) - } - } - ) } private fun logServiceInvocation( From 8186965f977745ea824f4c88b08781736054b410 Mon Sep 17 00:00:00 2001 From: Andrew Yu Date: Tue, 4 Mar 2025 15:12:50 -0800 Subject: [PATCH 2/2] fix test --- .../services/codewhisperer/CodeWhispererTelemetryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47a1f3eb754..954fc6a8dd8 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 @@ -376,7 +376,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() { 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()) + verify(popupManagerSpy, timeout(5000).atLeastOnce()).render(statesCaptor.capture(), any(), any(), any()) val states = statesCaptor.lastValue val metricCaptor = argumentCaptor() verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())