diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt index 6a9acbbfc07..f135cfa18b4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt @@ -11,6 +11,8 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeSca import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.UpdatePlaceholderMessage import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import java.util.UUID @@ -34,7 +36,7 @@ class CodeScanChatHelper( clearPreviousItemButtons: Boolean? = false, ) { if (isInValidSession()) return - + broadcastQEvent(QFeatureEvent.INVOCATION) messagePublisher.publish( CodeScanChatMessage( tabId = activeCodeScanTabId as String, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt index 76d7e9ef886..c6361c2ce85 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt @@ -71,6 +71,8 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendA import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.codewhisperer.util.isWithin import software.aws.toolkits.jetbrains.services.cwc.ChatConstants @@ -1205,6 +1207,7 @@ class CodeTestChatController( "Processing message: $message " + "tabId: $tabId" } + broadcastQEvent(QFeatureEvent.INVOCATION) when (session.conversationState) { ConversationState.WAITING_FOR_BUILD_COMMAND_INPUT -> handleBuildCommandInput(session, message) ConversationState.WAITING_FOR_REGENERATE_INPUT -> handleRegenerateInput(session, message) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt index 2d3729532ac..94825691014 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt @@ -70,6 +70,8 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Delete import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.resources.message import java.nio.file.Paths import java.util.UUID @@ -718,6 +720,7 @@ class DocController( is PrepareDocGenerationState -> state.filePaths else -> emptyList() } + broadcastQEvent(QFeatureEvent.INVOCATION) if (filePaths.isNotEmpty()) { processOpenDiff( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt index ddc3e2f4969..cead9106694 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt @@ -71,6 +71,8 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.content import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl @@ -191,6 +193,7 @@ class FeatureDevController( logger.debug { "$FEATURE_NAME: Processing InsertCodeAtCursorPosition: $message" } withContext(EDT) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext val caret: Caret = editor.caretModel.primaryCaret @@ -202,6 +205,7 @@ class FeatureDevController( } editor.document.insertString(offset, message.code) } + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } } @@ -679,7 +683,7 @@ class FeatureDevController( } session.preloader(message, messenger) - + broadcastQEvent(QFeatureEvent.INVOCATION) when (session.sessionState.phase) { SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId) else -> null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 7d7b28b664e..fc5450e247f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -49,6 +49,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextCo import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.InboundAppMessagesHandler import software.aws.toolkits.jetbrains.services.cwc.clients.chat.exceptions.ChatApiException import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData @@ -215,6 +217,7 @@ class ChatController private constructor( } override suspend fun processInsertCodeAtCursorPosition(message: IncomingCwcMessage.InsertCodeAtCursorPosition) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) withContext(EDT) { val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext @@ -245,6 +248,8 @@ class ChatController private constructor( } } telemetryHelper.recordInteractWithMessage(message) + + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } override suspend fun processStopResponseMessage(message: IncomingCwcMessage.StopResponse) { @@ -438,7 +443,7 @@ class ChatController private constructor( sessionInfo.history.add(requestData) telemetryHelper.recordEnterFocusConversation(tabId) telemetryHelper.recordStartConversation(tabId, requestData) - + broadcastQEvent(QFeatureEvent.INVOCATION) // Send the request to the API and publish the responses back to the UI. // This is launched in a scope attached to the sessionInfo so that the Job can be cancelled on a per-session basis. ChatPromptHandler(telemetryHelper).handle(tabId, triggerId, requestData, sessionInfo, shouldAddIndexInProgressMessage) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index 0b6c01b805b..72c2f4a0355 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -13,6 +13,8 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.codewhispererstreaming.model.CodeWhispererStreamingException import software.aws.toolkits.core.utils.convertMarkdownToHTML import software.aws.toolkits.core.utils.extractCodeBlockLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.clients.chat.exceptions.ChatApiException import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatResponseEvent @@ -115,6 +117,8 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { ) telemetryHelper.recordAddMessage(data, response, responseText.length, statusCode, countTotalNumberOfCodeBlocks(responseText)) emit(response) + + broadcastQEvent(QFeatureEvent.INVOCATION) } .catch { exception -> val statusCode = if (exception is AwsServiceException) exception.statusCode() else 0 diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index bcadef007ad..1a6f8d2d10f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -58,6 +58,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController @@ -191,7 +193,6 @@ class InlineChatController( private fun addPopupListeners(popup: JBPopup, editor: Editor) { val popupListener = object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { if (canPopupAbort.get() && event.asPopup().isDisposed) { popupCancelHandler.invoke(editor) @@ -534,6 +535,7 @@ class InlineChatController( private fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { lateinit var rangeMarker: RangeMarker + broadcastQEvent(QFeatureEvent.STARTS_EDITING) ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { @@ -543,11 +545,12 @@ class InlineChatController( highlightCodeWithBackgroundColor(editor, rangeMarker.startOffset, rangeMarker.endOffset, true) } } - + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) return rangeMarker } private fun replaceString(document: Document, start: Int, end: Int, text: String) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { @@ -555,6 +558,7 @@ class InlineChatController( } } } + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } private fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean) { @@ -711,6 +715,8 @@ class InlineChatController( canPopupAbort.set(true) undoChanges() } + + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) return errorMessage } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt index d0407a7c5a8..16be8937031 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt @@ -111,6 +111,8 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.toVirtualFi import software.aws.toolkits.jetbrains.services.codemodernizer.utils.tryGetJdk import software.aws.toolkits.jetbrains.services.codemodernizer.utils.unzipFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateSctMetadata +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import software.aws.toolkits.resources.message @@ -136,7 +138,7 @@ class CodeTransformChatController( if (objective == "language upgrade" || objective == "sql conversion") { telemetry.submitSelection(objective) } - + broadcastQEvent(QFeatureEvent.INVOCATION) when (objective) { "language upgrade" -> this.handleLanguageUpgrade() "sql conversion" -> this.handleSQLConversion() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt index f1d581673b9..ff1c0c72c60 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt @@ -38,6 +38,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhisp import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_ISSUE_TITLE_MAX_LENGTH @@ -331,6 +333,7 @@ fun applySuggestedFix(project: Project, issue: CodeWhispererCodeScanIssue) { try { val manager = CodeWhispererCodeReferenceManager.getInstance(issue.project) WriteCommandAction.runWriteCommandAction(issue.project) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) val document = FileDocumentManager.getInstance().getDocument(issue.file) ?: return@runWriteCommandAction val documentContent = document.text @@ -343,6 +346,7 @@ fun applySuggestedFix(project: Project, issue: CodeWhispererCodeScanIssue) { LOG.debug { "Original content from reference span: $originalContent" } manager.addReferenceLogPanelEntry(reference = reference, null, null, originalContent.split("\n")) } + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } if (issue.suggestedFixes[0].references.isNotEmpty()) { manager.toolWindow?.show() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt index cedd56e35a2..0115491f9af 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt @@ -134,6 +134,8 @@ interface CodeWhispererClientAdaptor : Disposable { acceptedTokenCount: Long, totalTokenCount: Long, unmodifiedAcceptedTokenCount: Long?, + userWrittenCodeCharacterCount: Long?, + userWrittenCodeLineCount: Long?, ): SendTelemetryEventResponse fun sendUserModificationTelemetry( @@ -481,6 +483,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW acceptedTokenCount: Long, totalTokenCount: Long, unmodifiedAcceptedTokenCount: Long?, + userWrittenCodeCharacterCount: Long?, + userWrittenCodeLineCount: Long?, ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder -> requestBuilder.telemetryEvent { telemetryEventBuilder -> telemetryEventBuilder.codeCoverageEvent { @@ -490,6 +494,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW it.totalCharacterCount(totalTokenCount.toInt()) it.timestamp(Instant.now()) it.unmodifiedAcceptedCharacterCount(unmodifiedAcceptedTokenCount?.toInt()) + it.userWrittenCodeCharacterCount(userWrittenCodeLineCount?.toInt()) + it.userWrittenCodeLineCount(userWrittenCodeLineCount?.toInt()) } } requestBuilder.optOutPreference(getTelemetryOptOutPreference()) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt index 323e66dfa4a..e8f28844972 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt @@ -15,6 +15,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmi import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatusNew import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererCodeCoverageTracker +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker class CodeWhispererEditorListener : EditorFactoryListener { override fun editorCreated(event: EditorFactoryEvent) { @@ -40,6 +41,10 @@ class CodeWhispererEditorListener : EditorFactoryListener { activateTrackerIfNotActive() documentChanged(event) } + UserWrittenCodeTracker.getInstance(project).apply { + activateTrackerIfNotActive() + documentChanged(event) + } } }, editor.disposable diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt index 22c8c2176bf..095a3e95e0c 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt @@ -15,6 +15,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationCo 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.telemetry.CodeWhispererTelemetryService +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_QUOTES @@ -44,9 +46,12 @@ class CodeWhispererEditorManager { val endOffsetToReplace = if (insertEndOffset != -1) insertEndOffset else primaryCaret.offset WriteCommandAction.runWriteCommandAction(project) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) document.replaceString(originalOffset, endOffsetToReplace, reformatted) PsiDocumentManager.getInstance(project).commitDocument(document) primaryCaret.moveToOffset(endOffset + detail.rightOverlap.length) + + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } ApplicationManager.getApplication().invokeLater { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt index a244c31232a..8e590fbba3b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManagerNew.kt @@ -16,6 +16,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispere import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManagerNew import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryServiceNew +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_QUOTES @@ -51,9 +53,12 @@ class CodeWhispererEditorManagerNew { preview.detail.isAccepted = true WriteCommandAction.runWriteCommandAction(project) { + broadcastQEvent(QFeatureEvent.STARTS_EDITING) document.replaceString(originalOffset, endOffsetToReplace, reformatted) PsiDocumentManager.getInstance(project).commitDocument(document) primaryCaret.moveToOffset(endOffset + detail.rightOverlap.length) + + broadcastQEvent(QFeatureEvent.FINISHES_EDITING) } ApplicationManager.getApplication().invokeLater { 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 f11c5014028..54451e949c0 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 @@ -74,6 +74,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeI import software.aws.toolkits.jetbrains.services.codewhisperer.model.WorkerContext import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeInsightsSettingsFacade import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants @@ -261,6 +263,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { lastRecommendationIndex += response.completions().size ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) .onSuccess(requestContext.fileContextInfo) + broadcastQEvent(QFeatureEvent.INVOCATION) CodeWhispererTelemetryService.getInstance().sendServiceInvocationEvent( requestId, requestContext, diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt index dc512c1ca9e..46bcc935743 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt @@ -253,7 +253,9 @@ abstract class CodeWhispererCodeCoverageTracker( customizationArn, acceptedCharsCount, totalCharsCount, - unmodifiedAcceptedCharsCount + unmodifiedAcceptedCharsCount, + 0, + 0 ) LOG.debug { "Successfully sent code percentage telemetry. RequestId: ${response.responseMetadata().requestId()}" } } catch (e: Exception) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt new file mode 100644 index 00000000000..5bcdd289c03 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/UserWrittenCodeTracker.kt @@ -0,0 +1,199 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.util.Alarm +import com.intellij.util.AlarmFactory +import com.intellij.util.concurrency.annotations.RequiresReadLock +import com.intellij.util.messages.MessageBusConnection +import com.intellij.util.messages.Topic +import org.jetbrains.annotations.TestOnly +import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException +import software.aws.toolkits.core.utils.debug +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor +import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker.Companion.Q_FEATURE_TOPIC +import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled +import software.aws.toolkits.jetbrains.settings.AwsSettings +import java.time.Duration +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +@Service(Service.Level.PROJECT) +class UserWrittenCodeTracker(private val project: Project) : Disposable { + val userWrittenCodeLineCount = mutableMapOf() + val userWrittenCodeCharacterCount = mutableMapOf() + private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this) + + private val isShuttingDown = AtomicBoolean(false) + val qInvocationCount: AtomicInteger = AtomicInteger(0) + private val isQMakingEdits = AtomicBoolean(false) + private val isActive: AtomicBoolean = AtomicBoolean(false) + private var conn: MessageBusConnection? = null + + @Synchronized + fun activateTrackerIfNotActive() { + // tracker will only be activated if and only if IsTelemetryEnabled = true && isActive = false + if (!isTelemetryEnabled() || isActive.get()) return + isActive.set(true) + // count q service invocations + conn = ApplicationManager.getApplication().messageBus.connect() + conn?.subscribe( + Q_FEATURE_TOPIC, + object : QFeatureListener { + override fun onEvent(event: QFeatureEvent) { + when (event) { + QFeatureEvent.INVOCATION -> qInvocationCount.getAndIncrement() + QFeatureEvent.STARTS_EDITING -> isQMakingEdits.set(true) + QFeatureEvent.FINISHES_EDITING -> isQMakingEdits.set(false) + } + } + } + ) + scheduleTracker() + } + + fun reset() { + userWrittenCodeLineCount.clear() + userWrittenCodeCharacterCount.clear() + qInvocationCount.set(0) + isQMakingEdits.set(false) + isActive.set(false) + isShuttingDown.set(false) + } + + private fun isTelemetryEnabled(): Boolean = AwsSettings.getInstance().isTelemetryEnabled + + private fun scheduleTracker() { + if (!alarm.isDisposed && !isShuttingDown.get()) { + alarm.addRequest({ flush() }, Duration.ofSeconds(DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS).toMillis()) + } + } + + private fun flush() { + try { + if (!isTelemetryEnabled() || qInvocationCount.get() <= 0) { + return + } + emitCodeWhispererCodeContribution() + } finally { + reset() + scheduleTracker() + } + } + + @RequiresReadLock + internal fun documentChanged(event: DocumentEvent) { + // do not listen to document changed made by Amazon Q itself + if (isQMakingEdits.get() || !isActive.get()) { + return + } + + // When open a file for the first time, IDE will also emit DocumentEvent for loading with `isWholeTextReplaced = true` + // Added this condition to filter out those events + if (event.isWholeTextReplaced) { + LOG.debug { "event with isWholeTextReplaced flag: $event" } + if (event.oldTimeStamp == 0L) return + } + // only count total tokens when it is a user keystroke input + // do not count doc changes from copy & paste of >=50 characters + // do not count other changes from formatter, git command, etc + // edge case: event can be from user hit enter with indentation where change is \n\t\t, count as 1 char increase in total chars + // when event is auto closing [{(', there will be 2 separated events, both count as 1 char increase in total chars + val text = event.newFragment.toString() + val lines = text.split('\n').size - 1 + if (event.newLength < COPY_THRESHOLD && !isIntelliJMultiSpacesInsert(text) && text.isNotEmpty()) { + // count doc changes from <50 multi character input as total user written code + // ignore all white space changes, this usually comes from IntelliJ formatting + val language = PsiDocumentManager.getInstance(project).getPsiFile(event.document)?.programmingLanguage() + if (language != null) { + userWrittenCodeLineCount[language] = userWrittenCodeLineCount.getOrDefault(language, 0) + lines + userWrittenCodeCharacterCount[language] = userWrittenCodeCharacterCount.getOrDefault(language, 0) + event.newLength + } + } + } + + // intelliJ sometimes insert multi spaces for indentation, this is not user written code + private fun isIntelliJMultiSpacesInsert(text: String) = text.trim { it == ' ' }.isEmpty() && text.length > 1 + + private fun emitCodeWhispererCodeContribution() { + val customizationArn: String? = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn + for ((language, _) in userWrittenCodeCharacterCount) { + if (userWrittenCodeCharacterCount.getOrDefault(language, 0) <= 0) { + continue + } + runIfIdcConnectionOrTelemetryEnabled(project) { + // here acceptedTokensSize is the count of accepted chars post user modification + try { + val response = CodeWhispererClientAdaptor.getInstance(project).sendCodePercentageTelemetry( + language, + customizationArn, + 0, + 0, + 0, + userWrittenCodeCharacterCount = userWrittenCodeCharacterCount.getOrDefault(language, 0), + userWrittenCodeLineCount = userWrittenCodeLineCount.getOrDefault(language, 0) + ) + LOG.debug { "Successfully sent code percentage telemetry. RequestId: ${response.responseMetadata().requestId()}" } + } catch (e: Exception) { + val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null + LOG.debug { + "Failed to send code percentage telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" + } + } + } + } + } + + companion object { + private const val DEFAULT_MODIFICATION_INTERVAL_IN_SECONDS = 300L // 5 minutes + + private const val COPY_THRESHOLD = 50 + private val LOG = getLogger() + + fun getInstance(project: Project) = project.service() + + val Q_FEATURE_TOPIC: Topic = Topic.create( + "Q service events", + QFeatureListener::class.java + ) + } + + override fun dispose() { + if (isShuttingDown.getAndSet(true)) { + return + } + conn?.disconnect() + flush() + } + + @TestOnly + fun forceTrackerFlush() { + alarm.drainRequestsInTest() + } +} + +enum class QFeatureEvent { + INVOCATION, + STARTS_EDITING, + FINISHES_EDITING, +} + +interface QFeatureListener { + fun onEvent(event: QFeatureEvent) +} + +fun broadcastQEvent(event: QFeatureEvent) = + ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC) + .onEvent(event) 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 854d74469ac..86ef92f8ccc 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 @@ -391,7 +391,7 @@ class CodeWhispererClientAdaptorTest { @Test fun `sendTelemetryEvent for codePercentage respects telemetry optin status`() { sendTelemetryEventOptOutCheckHelper { - sut.sendCodePercentageTelemetry(aProgrammingLanguage(), aString(), 0, 1, 0) + sut.sendCodePercentageTelemetry(aProgrammingLanguage(), aString(), 0, 1, 0, 0, 0) } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt new file mode 100644 index 00000000000..c01b70025de --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/UserWrittenCodeTrackerTest.kt @@ -0,0 +1,176 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.testFramework.DisposableRule +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.replaceService +import com.intellij.testFramework.runInEdtAndWait +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import software.aws.toolkits.core.telemetry.TelemetryBatcher +import software.aws.toolkits.core.telemetry.TelemetryPublisher +import software.aws.toolkits.jetbrains.core.MockClientManagerRule +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName +import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext +import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType +import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager +import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage +import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.UserWrittenCodeTracker.Companion.Q_FEATURE_TOPIC +import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher +import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService +import software.aws.toolkits.jetbrains.settings.AwsSettings +import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule + +internal class UserWrittenCodeTrackerTest { + + internal class TestTelemetryService( + publisher: TelemetryPublisher = NoOpPublisher(), + batcher: TelemetryBatcher, + ) : TelemetryService(publisher, batcher) + + @Rule + @JvmField + val disposableRule = DisposableRule() + + @Rule + @JvmField + val mockClientManagerRule = MockClientManagerRule() + + @Rule + @JvmField + var projectRule = PythonCodeInsightTestFixtureRule() + + lateinit var project: Project + lateinit var fixture: CodeInsightTestFixture + lateinit var telemetryServiceSpy: TelemetryService + lateinit var batcher: TelemetryBatcher + lateinit var exploreActionManagerMock: CodeWhispererExplorerActionManager + lateinit var sut: UserWrittenCodeTracker + + @Before + open fun setup() { + this.project = projectRule.project + this.fixture = projectRule.fixture + fixture.configureByText(pythonFileName, pythonTestLeftContext) + AwsSettings.getInstance().isTelemetryEnabled = true + batcher = mock() + + exploreActionManagerMock = mock { + on { checkActiveCodeWhispererConnectionType(any()) } doReturn CodeWhispererLoginType.Sono + } + + ApplicationManager.getApplication().replaceService(CodeWhispererExplorerActionManager::class.java, exploreActionManagerMock, disposableRule.disposable) + + fixture.configureByText(pythonFileName, pythonTestLeftContext) + runInEdtAndWait { + projectRule.fixture.editor.caretModel.primaryCaret.moveToOffset(projectRule.fixture.editor.document.textLength) + } + } + + @After + fun tearDown() { + if (::sut.isInitialized) { + sut.forceTrackerFlush() + sut.reset() + } + } + + @Test + fun `test tracker is listening to q service invocation`() { + sut = UserWrittenCodeTracker.getInstance(project) + sut.activateTrackerIfNotActive() + assertThat(sut.qInvocationCount.get()).isEqualTo(0) + ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.INVOCATION) + assertThat(sut.qInvocationCount.get()).isEqualTo(1) + ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.INVOCATION) + assertThat(sut.qInvocationCount.get()).isEqualTo(2) + } + + @Test + fun `test tracker is not listening to multi char input more than 50, but works for less than 50, and will not increment totalTokens - add new code`() { + sut = UserWrittenCodeTracker.getInstance(project) + sut.activateTrackerIfNotActive() + fixture.configureByText(pythonFileName, "") + val newCode = "def addTwoNumbers\n return" + runInEdtAndWait { + WriteCommandAction.runWriteCommandAction(project) { + fixture.editor.appendString(newCode) + } + } + val language: CodeWhispererProgrammingLanguage = CodeWhispererPython.INSTANCE + assertThat(sut.userWrittenCodeCharacterCount[language]).isEqualTo(newCode.length.toLong()) + assertThat(sut.userWrittenCodeLineCount[language]).isEqualTo(1) + + val anotherCode = "(x, y):\n".repeat(8) + runInEdtAndWait { + WriteCommandAction.runWriteCommandAction(project) { + fixture.editor.appendString(anotherCode) + } + } + assertThat(sut.userWrittenCodeCharacterCount[language]).isEqualTo(newCode.length.toLong()) + assertThat(sut.userWrittenCodeLineCount[language]).isEqualTo(1) + } + + @Test + fun `test tracker is listening to document changes and increment totalTokens - delete code should not affect`() { + sut = UserWrittenCodeTracker.getInstance(project) + sut.activateTrackerIfNotActive() + assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(CodeWhispererPython.INSTANCE, 0)).isEqualTo(0) + runInEdtAndWait { + fixture.editor.caretModel.primaryCaret.moveToOffset(fixture.editor.document.textLength) + WriteCommandAction.runWriteCommandAction(project) { + fixture.editor.document.deleteString(fixture.editor.caretModel.offset - 3, fixture.editor.caretModel.offset) + } + } + assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(CodeWhispererPython.INSTANCE, 0)).isEqualTo(0) + } + + @Test + fun `test tracker is listening to document changes only when Q is not editing`() { + sut = UserWrittenCodeTracker.getInstance(project) + sut.activateTrackerIfNotActive() + fixture.configureByText(pythonFileName, "") + val newCode = "def addTwoNumbers\n return" + + ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.STARTS_EDITING) + runInEdtAndWait { + WriteCommandAction.runWriteCommandAction(project) { + fixture.editor.appendString(newCode) + } + } + + ApplicationManager.getApplication().messageBus.syncPublisher(Q_FEATURE_TOPIC).onEvent(QFeatureEvent.FINISHES_EDITING) + val language: CodeWhispererProgrammingLanguage = CodeWhispererPython.INSTANCE + assertThat(sut.userWrittenCodeCharacterCount.getOrDefault(language, 0)).isEqualTo(0) + assertThat(sut.userWrittenCodeLineCount.getOrDefault(language, 0)).isEqualTo(0) + + runInEdtAndWait { + WriteCommandAction.runWriteCommandAction(project) { + fixture.editor.appendString(newCode) + } + } + assertThat(sut.userWrittenCodeCharacterCount[CodeWhispererPython.INSTANCE]).isEqualTo(newCode.length.toLong()) + assertThat(sut.userWrittenCodeLineCount[CodeWhispererPython.INSTANCE]).isEqualTo(1) + } + + private fun Editor.appendString(string: String) { + val currentOffset = caretModel.primaryCaret.offset + document.insertString(currentOffset, string) + caretModel.moveToOffset(currentOffset + string.length) + } +} diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json index 00e2305eb2d..bf28b26a6d7 100644 --- a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json +++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json @@ -579,7 +579,9 @@ "timestamp": { "shape": "Timestamp" }, "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }, "totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" }, - "totalNewCodeLineCount": { "shape": "PrimitiveInteger" } + "totalNewCodeLineCount": { "shape": "PrimitiveInteger" }, + "userWrittenCodeCharacterCount": { "shape": "PrimitiveInteger" }, + "userWrittenCodeLineCount": { "shape": "PrimitiveInteger" } } }, "CodeFixAcceptanceEvent": {