Skip to content

Commit 15c8a17

Browse files
Merge main into feature/dev-execution
2 parents c0d5ef0 + 805cb39 commit 15c8a17

File tree

9 files changed

+203
-25
lines changed

9 files changed

+203
-25
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Inline suggestions: Pre-fetch recommendations to reduce suggestion latency."
4+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ interface CodeWhispererClientAdaptor : Disposable {
7676
firstRequest: GenerateCompletionsRequest,
7777
): Sequence<GenerateCompletionsResponse>
7878

79+
fun generateCompletions(
80+
firstRequest: GenerateCompletionsRequest,
81+
): GenerateCompletionsResponse
82+
7983
fun createUploadUrl(
8084
request: CreateUploadUrlRequest,
8185
): CreateUploadUrlResponse
@@ -322,6 +326,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
322326
yield(response)
323327
} while (!nextToken.isNullOrEmpty())
324328
}
329+
override fun generateCompletions(firstRequest: GenerateCompletionsRequest): GenerateCompletionsResponse =
330+
bearerClient().generateCompletions(firstRequest)
325331

326332
override fun createUploadUrl(request: CreateUploadUrlRequest): CreateUploadUrlResponse =
327333
bearerClient().createUploadUrl(request)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.intellij.openapi.ui.popup.JBPopupListener
77
import com.intellij.openapi.ui.popup.LightweightWindowEvent
88
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
99
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
10+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
1011
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew
1112
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
1213
import java.time.Duration
@@ -27,7 +28,8 @@ class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopu
2728
recommendationContext,
2829
CodeWhispererPopupManager.getInstance().sessionContext,
2930
event.isOk,
30-
CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
31+
CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) },
32+
CodeWhispererService.getInstance().getNextInvocationContext()
3133
)
3234

3335
CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.Co
6969
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener
7070
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.addIntelliSenseAcceptListener
7171
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
72+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
7273
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
7374
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
7475
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX
@@ -460,6 +461,9 @@ class CodeWhispererPopupManager {
460461
CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(states, sessionContext)
461462
}
462463
closePopup(states.popup)
464+
if (sessionContext.selectedIndex == 0) {
465+
CodeWhispererService.getInstance().promoteNextInvocationIfAvailable()
466+
}
463467
}
464468
}
465469
)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.intellij.openapi.editor.Editor
1616
import com.intellij.openapi.editor.VisualPosition
1717
import com.intellij.openapi.project.Project
1818
import com.intellij.openapi.ui.popup.JBPopup
19+
import com.intellij.openapi.ui.popup.JBPopupFactory
1920
import com.intellij.openapi.util.Disposer
2021
import com.intellij.psi.PsiDocumentManager
2122
import com.intellij.psi.PsiFile
@@ -24,6 +25,7 @@ import com.intellij.util.messages.Topic
2425
import kotlinx.coroutines.CoroutineScope
2526
import kotlinx.coroutines.Deferred
2627
import kotlinx.coroutines.Job
28+
import kotlinx.coroutines.SupervisorJob
2729
import kotlinx.coroutines.async
2830
import kotlinx.coroutines.delay
2931
import kotlinx.coroutines.isActive
@@ -98,6 +100,7 @@ import java.util.concurrent.TimeUnit
98100
class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
99101
private val codeInsightSettingsFacade = CodeInsightsSettingsFacade()
100102
private var refreshFailure: Int = 0
103+
private var nextInvocationContext: InvocationContext? = null
101104

102105
init {
103106
Disposer.register(this, codeInsightSettingsFacade)
@@ -209,7 +212,118 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
209212
invokeCodeWhispererInBackground(requestContext)
210213
}
211214

212-
internal suspend fun invokeCodeWhispererInBackground(requestContext: RequestContext): Job {
215+
internal suspend fun invokeCodeWhispererInBackground(
216+
requestContext: RequestContext,
217+
currStates: InvocationContext? = null,
218+
): Job {
219+
// current states != null means that it's prefetch
220+
if (currStates != null) {
221+
val firstValidRecommendation = currStates.recommendationContext.details
222+
.firstOrNull {
223+
!it.isDiscarded && it.recommendation.content().isNotEmpty()
224+
} ?: return SupervisorJob().apply { complete() }
225+
val job = cs.launch(getCoroutineBgContext()) {
226+
val latencyContext = LatencyContext().apply {
227+
codewhispererPreprocessingStart = System.nanoTime()
228+
codewhispererEndToEndStart = System.nanoTime()
229+
}
230+
231+
val nextCaretPosition = CaretPosition(
232+
line = requestContext.caretPosition.line + firstValidRecommendation.recommendation.content().count { it == '\n' },
233+
offset = requestContext.caretPosition.offset + firstValidRecommendation.recommendation.content().length
234+
)
235+
236+
val nextFileContextInfo = requestContext.fileContextInfo.copy(
237+
caretContext = requestContext.fileContextInfo.caretContext.copy(
238+
leftFileContext = requestContext.fileContextInfo.caretContext.leftFileContext + firstValidRecommendation.recommendation.content()
239+
)
240+
)
241+
242+
val nextRequestContext = requestContext.copy(
243+
caretPosition = nextCaretPosition,
244+
fileContextInfo = nextFileContextInfo,
245+
latencyContext = latencyContext
246+
)
247+
val newVisualPosition = withContext(EDT) {
248+
runReadAction {
249+
nextRequestContext.editor.offsetToVisualPosition(nextRequestContext.caretPosition.offset)
250+
}
251+
}
252+
try {
253+
val nextResponse = CodeWhispererClientAdaptor
254+
.getInstance(nextRequestContext.project)
255+
.generateCompletions(
256+
buildCodeWhispererRequest(
257+
nextRequestContext.fileContextInfo,
258+
nextRequestContext.awaitSupplementalContext(),
259+
nextRequestContext.customizationArn
260+
)
261+
)
262+
val startTime = System.nanoTime()
263+
nextRequestContext.latencyContext.codewhispererPreprocessingEnd = System.nanoTime()
264+
nextRequestContext.latencyContext.paginationAllCompletionsStart = System.nanoTime()
265+
CodeWhispererInvocationStatus.getInstance().setInvocationStart()
266+
nextResponse.let {
267+
val endTime = System.nanoTime()
268+
val latency = TimeUnit.NANOSECONDS.toMillis(endTime - startTime).toDouble()
269+
val requestId = nextResponse.responseMetadata().requestId()
270+
val sessionId = nextResponse.sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
271+
272+
nextRequestContext.latencyContext.apply {
273+
codewhispererPostprocessingStart = System.nanoTime()
274+
paginationFirstCompletionTime = (endTime - codewhispererEndToEndStart).toDouble()
275+
firstRequestId = requestId
276+
}
277+
278+
CodeWhispererInvocationStatus.getInstance().setInvocationSessionId(sessionId)
279+
280+
val nextResponseContext = ResponseContext(sessionId)
281+
CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent(
282+
nextResponse.responseMetadata().requestId(),
283+
nextRequestContext,
284+
nextResponseContext,
285+
nextResponse.completions().size,
286+
invocationSuccess = true,
287+
latency,
288+
null
289+
)
290+
val validatedResponse = validateResponse(it)
291+
val detailContexts = withContext(EDT) {
292+
runReadAction {
293+
CodeWhispererRecommendationManager.getInstance().buildDetailContext(
294+
nextRequestContext,
295+
"",
296+
validatedResponse.completions(),
297+
validatedResponse.responseMetadata().requestId()
298+
)
299+
}
300+
}
301+
val nextRecommendationContext = RecommendationContext(detailContexts, "", "", newVisualPosition)
302+
val newPopup = withContext(EDT) {
303+
JBPopupFactory.getInstance().createMessage("Dummy popup")
304+
}
305+
306+
// send userDecision and trigger decision when next recommendation haven't been seen
307+
if (currStates.popup.isDisposed) {
308+
CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
309+
nextRequestContext,
310+
nextResponseContext,
311+
nextRecommendationContext,
312+
SessionContext(),
313+
false
314+
)
315+
} else {
316+
nextInvocationContext = InvocationContext(nextRequestContext, nextResponseContext, nextRecommendationContext, newPopup)
317+
}
318+
LOG.debug { "Prefetched next invocation stored in nextInvocationContext" }
319+
}
320+
} catch (ex: Exception) {
321+
LOG.warn { "Failed to prefetch next codewhisperer invocation: ${ex.message}" }
322+
}
323+
}
324+
return job
325+
}
326+
213327
val popup = withContext(EDT) {
214328
CodeWhispererPopupManager.getInstance().initPopup().also {
215329
Disposer.register(it) { CodeWhispererInvocationStatus.getInstance().finishInvocation() }
@@ -491,6 +605,9 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
491605
CodeWhispererPopupManager.getInstance().cancelPopup(popup)
492606
return null
493607
}
608+
cs.launch(getCoroutineBgContext()) {
609+
invokeCodeWhispererInBackground(requestContext, nextStates)
610+
}
494611
} else {
495612
// subsequent responses
496613
nextStates = updateStates(currStates, response)
@@ -616,6 +733,34 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
616733
CodeWhispererPopupManager.getInstance().changeStates(states, 0, recommendationAdded)
617734
}
618735

736+
fun promoteNextInvocationIfAvailable() {
737+
val nextStates = nextInvocationContext ?: run {
738+
LOG.debug { "No nextInvocationContext found, nothing to promote." }
739+
return
740+
}
741+
nextInvocationContext?.popup?.let { Disposer.dispose(it) }
742+
nextInvocationContext = null
743+
744+
cs.launch {
745+
val newPopup = CodeWhispererPopupManager.getInstance().initPopup()
746+
val updatedNextStates = nextStates.copy(popup = newPopup).also {
747+
addPopupChildDisposables(it.requestContext.project, it.requestContext.editor, it.popup)
748+
Disposer.register(newPopup, it)
749+
}
750+
CodeWhispererPopupManager.getInstance().initPopupListener(updatedNextStates)
751+
withContext(EDT) {
752+
CodeWhispererPopupManager.getInstance().changeStates(
753+
updatedNextStates,
754+
0,
755+
recommendationAdded = false
756+
)
757+
}
758+
invokeCodeWhispererInBackground(updatedNextStates.requestContext, updatedNextStates)
759+
}
760+
761+
LOG.debug { "Promoted nextInvocationContext to current session and displayed next recommendation." }
762+
}
763+
619764
private fun sendDiscardedUserDecisionEventForAll(
620765
requestContext: RequestContext,
621766
responseContext: ResponseContext,
@@ -782,6 +927,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
782927

783928
override fun dispose() {}
784929

930+
fun getNextInvocationContext(): InvocationContext? = nextInvocationContext
931+
785932
companion object {
786933
private val LOG = getLogger<CodeWhispererService>()
787934
private const val MAX_REFRESH_ATTEMPT = 3

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ class CodeWhispererTelemetryService {
459459
sessionContext: SessionContext,
460460
hasUserAccepted: Boolean,
461461
popupShownTime: Duration? = null,
462+
nextInvocationContext: InvocationContext? = null,
462463
) {
463464
val detailContexts = recommendationContext.details
464465
val decisions = mutableListOf<CodewhispererSuggestionState>()
@@ -506,6 +507,19 @@ class CodeWhispererTelemetryService {
506507
previousUserTriggerDecisions.add(this)
507508
// we need this as well because AutotriggerService will reset the queue periodically
508509
CodeWhispererAutoTriggerService.getInstance().addPreviousDecision(this)
510+
// send possible next session event if current action is reject and next popup haven't shown up
511+
if (CodewhispererSuggestionState.from(this.toString()) == CodewhispererSuggestionState.Reject) {
512+
nextInvocationContext?.let {
513+
sendUserDecisionEventForAll(
514+
it.requestContext,
515+
it.responseContext,
516+
it.recommendationContext,
517+
SessionContext(),
518+
false,
519+
nextInvocationContext = null
520+
)
521+
}
522+
}
509523
}
510524
}
511525

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package software.aws.toolkits.jetbrains.services.codewhisperer
55

66
import com.intellij.analysis.problemsView.toolWindow.ProblemsView
7-
import com.intellij.openapi.application.ApplicationManager
87
import com.intellij.openapi.components.service
98
import com.intellij.openapi.wm.RegisterToolWindowTask
109
import com.intellij.openapi.wm.ToolWindow
@@ -20,14 +19,12 @@ import org.junit.Ignore
2019
import org.junit.Test
2120
import org.mockito.kotlin.any
2221
import org.mockito.kotlin.never
23-
import org.mockito.kotlin.spy
2422
import org.mockito.kotlin.verify
2523
import org.mockito.kotlin.whenever
2624
import software.aws.toolkits.jetbrains.core.ToolWindowHeadlessManagerImpl
2725
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
2826
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState
2927
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
30-
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
3128
import software.aws.toolkits.jetbrains.services.codewhisperer.status.CodeWhispererStatusBarWidgetFactory
3229
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceToolWindowFactory
3330
import software.aws.toolkits.jetbrains.settings.CodeWhispererConfiguration
@@ -37,14 +34,11 @@ import kotlin.test.fail
3734

3835
class CodeWhispererSettingsTest : CodeWhispererTestBase() {
3936

40-
private lateinit var codewhispererServiceSpy: CodeWhispererService
4137
private lateinit var toolWindowHeadlessManager: ToolWindowHeadlessManagerImpl
4238

4339
@Before
4440
override fun setUp() {
4541
super.setUp()
46-
codewhispererServiceSpy = spy(codewhispererService)
47-
ApplicationManager.getApplication().replaceService(CodeWhispererService::class.java, codewhispererServiceSpy, disposableRule.disposable)
4842

4943
// Create a mock ToolWindowManager with working implementation of setAvailable() and isAvailable()
5044
toolWindowHeadlessManager = object : ToolWindowHeadlessManagerImpl(projectRule.project) {
@@ -86,7 +80,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
8680
whenever(stateManager.checkActiveCodeWhispererConnectionType(projectRule.project)).thenReturn(CodeWhispererLoginType.Logout)
8781
assertThat(isCodeWhispererEnabled(projectRule.project)).isFalse
8882
invokeCodeWhispererService()
89-
verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any())
83+
verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any())
9084
}
9185

9286
@Test
@@ -95,7 +89,7 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
9589
assertThat(stateManager.isAutoEnabled()).isFalse
9690
runInEdtAndWait {
9791
projectRule.fixture.type(':')
98-
verify(codewhispererServiceSpy, never()).showRecommendationsInPopup(any(), any(), any())
92+
verify(codewhispererService, never()).showRecommendationsInPopup(any(), any(), any())
9993
}
10094
}
10195

0 commit comments

Comments
 (0)