diff --git a/src/codewhisperer/activation.ts b/src/codewhisperer/activation.ts index 215e028fca..93f53f6344 100644 --- a/src/codewhisperer/activation.ts +++ b/src/codewhisperer/activation.ts @@ -75,6 +75,10 @@ export async function activate(context: ExtContext): Promise { */ const client = new codewhispererClient.DefaultCodeWhispererClient() + // Service initialization + ReferenceInlineProvider.instance + ImportAdderProvider.instance + context.extensionContext.subscriptions.push( /** * Configuration change @@ -176,7 +180,7 @@ export async function activate(context: ExtContext): Promise { if (isInlineCompletionEnabled() && e.uri.fsPath !== InlineCompletionService.instance.filePath()) { return } - RecommendationHandler.instance.reportUserDecisionOfRecommendation(vscode.window.activeTextEditor, -1) + RecommendationHandler.instance.reportUserDecisions(-1) }), vscode.languages.registerHoverProvider( @@ -259,49 +263,49 @@ export async function activate(context: ExtContext): Promise { await RecommendationHandler.instance.onCursorChange(e) }), vscode.workspace.onDidChangeTextDocument(async e => { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + if (e.document !== editor.document) { + return + } + if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId)) { + return + } + /** * CodeWhisperer security panel dynamic handling */ - if (e.document === vscode.window.activeTextEditor?.document) { - disposeSecurityDiagnostic(e) - } + disposeSecurityDiagnostic(e) CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) /** * Handle this keystroke event only when - * 1. It is in current non plaintext active editor - * 2. It is not a backspace - * 3. It is not caused by CodeWhisperer editing - * 4. It is not from undo/redo. + * 1. It is not a backspace + * 2. It is not caused by CodeWhisperer editing + * 3. It is not from undo/redo. */ - if ( - e.document === vscode.window.activeTextEditor?.document && - runtimeLanguageContext.isLanguageSupported(e.document.languageId) && - e.contentChanges.length != 0 && - !vsCodeState.isCodeWhispererEditing - ) { - if (vsCodeState.lastUserModificationTime) { - TelemetryHelper.instance.setTimeSinceLastModification( - performance.now() - vsCodeState.lastUserModificationTime - ) - } - vsCodeState.lastUserModificationTime = performance.now() - /** - * Important: Doing this sleep(10) is to make sure - * 1. this event is processed by vs code first - * 2. editor.selection.active has been successfully updated by VS Code - * Then this event can be processed by our code. - */ - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - if (!RecommendationHandler.instance.isSuggestionVisible()) { - await KeyStrokeHandler.instance.processKeyStroke( - e, - vscode.window.activeTextEditor, - client, - await getConfigEntry() - ) - } + if (e.contentChanges.length === 0 || vsCodeState.isCodeWhispererEditing) { + return + } + + if (vsCodeState.lastUserModificationTime) { + TelemetryHelper.instance.setTimeSinceLastModification( + performance.now() - vsCodeState.lastUserModificationTime + ) + } + vsCodeState.lastUserModificationTime = performance.now() + /** + * Important: Doing this sleep(10) is to make sure + * 1. this event is processed by vs code first + * 2. editor.selection.active has been successfully updated by VS Code + * Then this event can be processed by our code. + */ + await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + if (!RecommendationHandler.instance.isSuggestionVisible()) { + await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) } }) ) @@ -327,39 +331,33 @@ export async function activate(context: ExtContext): Promise { * Automated trigger */ vscode.workspace.onDidChangeTextDocument(async e => { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + if (e.document !== editor.document) { + return + } + if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId)) { + return + } /** * CodeWhisperer security panel dynamic handling */ - if (e.document === vscode.window.activeTextEditor?.document) { - if (isCloud9()) { - securityPanelViewProvider.disposeSecurityPanelItem(e, vscode.window.activeTextEditor) - } else { - disposeSecurityDiagnostic(e) - } - } - + securityPanelViewProvider.disposeSecurityPanelItem(e, editor) CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) - if ( - e.document === vscode.window.activeTextEditor?.document && - runtimeLanguageContext.isLanguageSupported(e.document.languageId) && - e.contentChanges.length != 0 && - !vsCodeState.isCodeWhispererEditing - ) { - /** - * Important: Doing this sleep(10) is to make sure - * 1. this event is processed by vs code first - * 2. editor.selection.active has been successfully updated by VS Code - * Then this event can be processed by our code. - */ - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - await KeyStrokeHandler.instance.processKeyStroke( - e, - vscode.window.activeTextEditor, - client, - await getConfigEntry() - ) + if (e.contentChanges.length != 0 && !vsCodeState.isCodeWhispererEditing) { + return } + /** + * Important: Doing this sleep(10) is to make sure + * 1. this event is processed by vs code first + * 2. editor.selection.active has been successfully updated by VS Code + * Then this event can be processed by our code. + */ + await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) }), /** @@ -389,7 +387,7 @@ export async function activate(context: ExtContext): Promise { } export async function shutdown() { - RecommendationHandler.instance.reportUserDecisionOfRecommendation(vscode.window.activeTextEditor, -1) + RecommendationHandler.instance.reportUserDecisions(-1) CodeWhispererTracker.getTracker().shutdown() } diff --git a/src/codewhisperer/commands/invokeRecommendation.ts b/src/codewhisperer/commands/invokeRecommendation.ts index 39d389e71e..7d893e4574 100644 --- a/src/codewhisperer/commands/invokeRecommendation.ts +++ b/src/codewhisperer/commands/invokeRecommendation.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' -import { vsCodeState, ConfigurationEntry } from '../models/model' +import { vsCodeState, ConfigurationEntry, GetRecommendationsResponse } from '../models/model' import { resetIntelliSenseState } from '../util/globalStateUtil' import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { isCloud9 } from '../../shared/extensionUtilities' @@ -64,8 +64,9 @@ export async function invokeRecommendation( vsCodeState.isIntelliSenseActive = false RecommendationHandler.instance.isGenerateRecommendationInProgress = true try { + let response: GetRecommendationsResponse if (isCloud9('classic') || isIamConnection(AuthUtil.instance.conn)) { - await RecommendationHandler.instance.getRecommendations( + response = await RecommendationHandler.instance.getRecommendations( client, editor, 'OnDemand', @@ -77,7 +78,7 @@ export async function invokeRecommendation( if (AuthUtil.instance.isConnectionExpired()) { await AuthUtil.instance.showReauthenticatePrompt() } - await RecommendationHandler.instance.getRecommendations( + response = await RecommendationHandler.instance.getRecommendations( client, editor, 'OnDemand', @@ -86,7 +87,7 @@ export async function invokeRecommendation( true ) } - if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, true)) { + if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, true, response)) { await vscode.commands.executeCommand('editor.action.triggerSuggest').then(() => { vsCodeState.isIntelliSenseActive = true }) diff --git a/src/codewhisperer/commands/onAcceptance.ts b/src/codewhisperer/commands/onAcceptance.ts index e05046bf28..b72f4b0f0b 100644 --- a/src/codewhisperer/commands/onAcceptance.ts +++ b/src/codewhisperer/commands/onAcceptance.ts @@ -83,8 +83,5 @@ export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEn } // at the end of recommendation acceptance, report user decisions and clear recommendations. - RecommendationHandler.instance.reportUserDecisionOfRecommendation( - acceptanceEntry.editor, - acceptanceEntry.acceptIndex - ) + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) } diff --git a/src/codewhisperer/commands/onInlineAcceptance.ts b/src/codewhisperer/commands/onInlineAcceptance.ts index 9bc7c0e6a5..c3dc03741d 100644 --- a/src/codewhisperer/commands/onInlineAcceptance.ts +++ b/src/codewhisperer/commands/onInlineAcceptance.ts @@ -129,9 +129,6 @@ export async function onInlineAcceptance( ) } - RecommendationHandler.instance.reportUserDecisionOfRecommendation( - acceptanceEntry.editor, - acceptanceEntry.acceptIndex - ) + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) } } diff --git a/src/codewhisperer/models/constants.ts b/src/codewhisperer/models/constants.ts index a4ab565275..e60ecf2dc8 100644 --- a/src/codewhisperer/models/constants.ts +++ b/src/codewhisperer/models/constants.ts @@ -217,7 +217,7 @@ export const artifactTypeSource = 'SourceCode' export const codeScanFindingsSchema = 'codescan/findings/1.0' // wait time for editor to update editor.selection.active (in milliseconds) -export const vsCodeCursorUpdateDelay = 3 +export const vsCodeCursorUpdateDelay = 10 export const reloadWindow = 'Reload Now' diff --git a/src/codewhisperer/models/model.ts b/src/codewhisperer/models/model.ts index 9e7d84260a..a4832c36e8 100644 --- a/src/codewhisperer/models/model.ts +++ b/src/codewhisperer/models/model.ts @@ -36,6 +36,12 @@ export const vsCodeState: VsCodeState = { lastUserModificationTime: 0, } +// This response struct can contain more info as needed +export interface GetRecommendationsResponse { + readonly result: 'Succeeded' | 'Failed' + readonly errorMessage: string | undefined +} + export interface AcceptedSuggestionEntry { readonly time: Date readonly fileUrl: vscode.Uri diff --git a/src/codewhisperer/service/importAdderProvider.ts b/src/codewhisperer/service/importAdderProvider.ts index 218103df73..a13e8ffe14 100644 --- a/src/codewhisperer/service/importAdderProvider.ts +++ b/src/codewhisperer/service/importAdderProvider.ts @@ -8,6 +8,7 @@ import { isCloud9 } from '../../shared/extensionUtilities' import { Recommendation } from '../client/codewhisperer' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { findLineToInsertImportStatement } from '../util/importAdderUtil' +import { application } from '../util/codeWhispererApplication' /** * ImportAdderProvider @@ -27,6 +28,12 @@ export class ImportAdderProvider implements vscode.CodeLensProvider { static #instance: ImportAdderProvider + constructor() { + application().clearCodeWhispererUIListener(_ => { + this.clear() + }) + } + public static get instance() { return (this.#instance ??= new this()) } diff --git a/src/codewhisperer/service/inlineCompletionItemProvider.ts b/src/codewhisperer/service/inlineCompletionItemProvider.ts index 6064b53476..8de3a69808 100644 --- a/src/codewhisperer/service/inlineCompletionItemProvider.ts +++ b/src/codewhisperer/service/inlineCompletionItemProvider.ts @@ -10,6 +10,7 @@ import { TelemetryHelper } from '../util/telemetryHelper' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { ReferenceInlineProvider } from './referenceInlineProvider' import { ImportAdderProvider } from './importAdderProvider' +import { application } from '../util/codeWhispererApplication' export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { private activeItemIndex: number | undefined @@ -133,8 +134,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt token: vscode.CancellationToken ): vscode.ProviderResult { if (position.line < 0 || position.isBefore(this.startPos)) { - ReferenceInlineProvider.instance.removeInlineReference() - ImportAdderProvider.instance.clear() + application()._clearCodeWhispererUIListener.fire() this.activeItemIndex = undefined return } @@ -164,7 +164,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) this.nextMove = 0 TelemetryHelper.instance.setFirstSuggestionShowTime() - TelemetryHelper.instance.tryRecordClientComponentLatency(document.languageId) + TelemetryHelper.instance.tryRecordClientComponentLatency() this._onDidShow.fire() if (matchedCount >= 2 || this.nextToken !== '') { const result = [item] @@ -175,8 +175,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt } return [item] } - ReferenceInlineProvider.instance.removeInlineReference() - ImportAdderProvider.instance.clear() + application()._clearCodeWhispererUIListener.fire() this.activeItemIndex = undefined return [] } diff --git a/src/codewhisperer/service/inlineCompletionService.ts b/src/codewhisperer/service/inlineCompletionService.ts index c5b1d408df..d6e26e3b1f 100644 --- a/src/codewhisperer/service/inlineCompletionService.ts +++ b/src/codewhisperer/service/inlineCompletionService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { ConfigurationEntry, vsCodeState } from '../models/model' +import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' import * as CodeWhispererConstants from '../models/constants' import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { RecommendationHandler } from './recommendationHandler' @@ -15,6 +15,7 @@ import { AuthUtil } from '../util/authUtil' import { shared } from '../../shared/utilities/functionUtils' import { ClassifierTrigger } from './classifierTrigger' import { session } from '../util/codeWhispererSession' +import { noSuggestions } from '../models/constants' const performance = globalThis.performance ?? require('perf_hooks').performance @@ -80,6 +81,10 @@ export class InlineCompletionService { ) { return } + + // Call report user decisions once to report recommendations leftover from last invocation. + RecommendationHandler.instance.reportUserDecisions(-1) + this.setCodeWhispererStatusBarLoading() if (ClassifierTrigger.instance.shouldInvokeClassifier(editor.document.languageId)) { ClassifierTrigger.instance.recordClassifierResultForAutoTrigger(editor, autoTriggerType, event) @@ -96,10 +101,14 @@ export class InlineCompletionService { TelemetryHelper.instance.setInvocationStartTime(performance.now()) RecommendationHandler.instance.checkAndResetCancellationTokens() RecommendationHandler.instance.documentUri = editor.document.uri + let response: GetRecommendationsResponse = { + result: 'Failed', + errorMessage: undefined, + } try { let page = 0 while (page < this.maxPage) { - await RecommendationHandler.instance.getRecommendations( + response = await RecommendationHandler.instance.getRecommendations( client, editor, triggerType, @@ -109,8 +118,8 @@ export class InlineCompletionService { page ) if (RecommendationHandler.instance.checkAndResetCancellationTokens()) { - // RecommendationHandler.instance.reportUserDecisionOfRecommendation(editor, -1) - // vscode.commands.executeCommand('aws.codeWhisperer.refreshStatusBar') + RecommendationHandler.instance.reportUserDecisions(-1) + vscode.commands.executeCommand('aws.codeWhisperer.refreshStatusBar') TelemetryHelper.instance.setIsRequestCancelled(true) return } @@ -125,13 +134,9 @@ export class InlineCompletionService { } vscode.commands.executeCommand('aws.codeWhisperer.refreshStatusBar') if (triggerType === 'OnDemand' && session.recommendations.length === 0) { - if (RecommendationHandler.instance.errorMessagePrompt !== '') { - showTimedMessage(RecommendationHandler.instance.errorMessagePrompt, 2000) - } else { - showTimedMessage(CodeWhispererConstants.noSuggestions, 2000) - } + showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) } - TelemetryHelper.instance.tryRecordClientComponentLatency(editor.document.languageId) + TelemetryHelper.instance.tryRecordClientComponentLatency() } setCodeWhispererStatusBarLoading() { diff --git a/src/codewhisperer/service/keyStrokeHandler.ts b/src/codewhisperer/service/keyStrokeHandler.ts index ae7757f40c..13acb0d92d 100644 --- a/src/codewhisperer/service/keyStrokeHandler.ts +++ b/src/codewhisperer/service/keyStrokeHandler.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import { DefaultCodeWhispererClient } from '../client/codewhisperer' import * as CodeWhispererConstants from '../models/constants' -import { vsCodeState, ConfigurationEntry } from '../models/model' +import { vsCodeState, ConfigurationEntry, GetRecommendationsResponse } from '../models/model' import { getLogger } from '../../shared/logger' import { isCloud9 } from '../../shared/extensionUtilities' import { RecommendationHandler } from './recommendationHandler' @@ -27,7 +27,7 @@ const performance = globalThis.performance ?? require('perf_hooks').performance */ export class KeyStrokeHandler { /** - * Speical character which automated triggers codewhisperer + * Special character which automated triggers codewhisperer */ public specialChar: string /** @@ -142,7 +142,6 @@ export class KeyStrokeHandler { this.invokeAutomatedTrigger(triggerType, editor, client, config, event) } } catch (error) { - getLogger().error('Automated Trigger Exception : ', error) getLogger().verbose(`Automated Trigger Exception : ${error}`) } } @@ -166,8 +165,9 @@ export class KeyStrokeHandler { vsCodeState.isIntelliSenseActive = false RecommendationHandler.instance.isGenerateRecommendationInProgress = true try { + let response: GetRecommendationsResponse if (isCloud9('classic') || isIamConnection(AuthUtil.instance.conn)) { - await RecommendationHandler.instance.getRecommendations( + response = await RecommendationHandler.instance.getRecommendations( client, editor, 'AutoTrigger', @@ -179,7 +179,7 @@ export class KeyStrokeHandler { if (AuthUtil.instance.isConnectionExpired()) { await AuthUtil.instance.showReauthenticatePrompt() } - await RecommendationHandler.instance.getRecommendations( + response = await RecommendationHandler.instance.getRecommendations( client, editor, 'AutoTrigger', @@ -188,7 +188,7 @@ export class KeyStrokeHandler { true ) } - if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, false)) { + if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, false, response)) { await vscode.commands.executeCommand('editor.action.triggerSuggest').then(() => { vsCodeState.isIntelliSenseActive = true }) diff --git a/src/codewhisperer/service/recommendationHandler.ts b/src/codewhisperer/service/recommendationHandler.ts index 0849d7f77d..35fe60539e 100644 --- a/src/codewhisperer/service/recommendationHandler.ts +++ b/src/codewhisperer/service/recommendationHandler.ts @@ -8,7 +8,7 @@ import { extensionVersion } from '../../shared/vscode/env' import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' import * as EditorContext from '../util/editorContext' import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry, vsCodeState } from '../models/model' +import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { AWSError } from 'aws-sdk' import { isAwsError } from '../../shared/errors' @@ -25,20 +25,18 @@ import { CodewhispererAutomatedTriggerType, CodewhispererCompletionType, CodewhispererTriggerType, - Result, telemetry, } from '../../shared/telemetry/telemetry' import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' import { session } from '../util/codeWhispererSession' -import { ReferenceInlineProvider } from './referenceInlineProvider' -import { ImportAdderProvider } from './importAdderProvider' import { Commands } from '../../shared/vscode/commands2' import globals from '../../shared/extensionGlobals' -import { updateInlineLockKey } from '../models/constants' +import { noSuggestions, updateInlineLockKey } from '../models/constants' import AsyncLock from 'async-lock' import { AuthUtil } from '../util/authUtil' import { CodeWhispererUserGroupSettings } from '../util/userGroupUtil' import { CWInlineCompletionItemProvider } from './inlineCompletionItemProvider' +import { application } from '../util/codeWhispererApplication' /** * This class is for getRecommendation/listRecommendation API calls and its states @@ -56,7 +54,7 @@ const nextCommand = Commands.declare('editor.action.inlineSuggest.showNext', () }) const rejectCommand = Commands.declare('aws.codeWhisperer.rejectCodeSuggestion', () => async () => { - RecommendationHandler.instance.reportUserDecisionOfRecommendation(vscode.window.activeTextEditor, -1) + RecommendationHandler.instance.reportUserDecisions(-1) }) const lock = new AsyncLock({ maxPending: 1 }) @@ -65,9 +63,7 @@ export class RecommendationHandler { public lastInvocationTime: number public requestId: string private nextToken: string - public errorCode: string private cancellationToken: vscode.CancellationTokenSource - public errorMessagePrompt: string public isGenerateRecommendationInProgress: boolean private _onDidReceiveRecommendation: vscode.EventEmitter = new vscode.EventEmitter() public readonly onDidReceiveRecommendation: vscode.Event = this._onDidReceiveRecommendation.event @@ -82,10 +78,8 @@ export class RecommendationHandler { constructor() { this.requestId = '' this.nextToken = '' - this.errorCode = '' this.lastInvocationTime = performance.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 this.cancellationToken = new vscode.CancellationTokenSource() - this.errorMessagePrompt = '' this.isGenerateRecommendationInProgress = false this.prev = new vscode.Disposable(() => {}) this.next = new vscode.Disposable(() => {}) @@ -148,27 +142,25 @@ export class RecommendationHandler { autoTriggerType?: CodewhispererAutomatedTriggerType, pagination: boolean = true, page: number = 0 - ) { + ): Promise { + let invocationResult: 'Succeeded' | 'Failed' = 'Failed' + let errorMessage: string | undefined = undefined + if (!editor) { - return + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + }) } - - let recommendation: RecommendationsList = [] + let recommendations: RecommendationsList = [] let requestId = '' let sessionId = '' - let invocationResult: Result = 'Failed' let reason = '' let startTime = 0 let latency = 0 let nextToken = '' - let errorCode = '' - let shouldRecordServiceInvocation = false - const languageContext = runtimeLanguageContext.getLanguageContext(editor.document.languageId) - - // set start pos for non pagination call or first pagination call - if (!pagination || (pagination && page === 0)) { - session.startPos = editor.selection.active - } + let shouldRecordServiceInvocation = true + session.language = runtimeLanguageContext.getLanguageContext(editor.document.languageId).language if (pagination) { if (page === 0) { @@ -192,55 +184,62 @@ export class RecommendationHandler { } const request = session.requestContext.request - try { - startTime = performance.now() - this.lastInvocationTime = startTime + // set start pos for non pagination call or first pagination call + if (!pagination || (pagination && page === 0)) { + session.startPos = editor.selection.active + session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) + /** * Validate request */ - if (EditorContext.validateRequest(request)) { - const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) - const codewhispererPromise = pagination - ? client.listRecommendations(mappedReq) - : client.generateRecommendations(mappedReq) - shouldRecordServiceInvocation = true - const resp = await this.getServerResponse( - triggerType, - config.isManualTriggerEnabled, - page === 0, - codewhispererPromise - ) - TelemetryHelper.instance.setSdkApiCallEndTime() - latency = startTime !== 0 ? performance.now() - startTime : 0 - if ('recommendations' in resp) { - recommendation = (resp && resp.recommendations) || [] - } else { - recommendation = (resp && resp.completions) || [] - } - invocationResult = 'Succeeded' - TelemetryHelper.instance.triggerType = triggerType - TelemetryHelper.instance.CodeWhispererAutomatedtriggerType = - autoTriggerType === undefined ? 'KeyStrokeCount' : autoTriggerType - requestId = resp?.$response && resp?.$response?.requestId - nextToken = resp?.nextToken ? resp?.nextToken : '' - sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] - TelemetryHelper.instance.setFirstResponseRequestId(requestId) - if (page === 0) { - TelemetryHelper.instance.setTimeToFirstRecommendation(performance.now()) - } - if (nextToken === '') { - TelemetryHelper.instance.setLastRequestId(requestId) - TelemetryHelper.instance.setAllPaginationEndTime() - } - } else { - getLogger().info('Invalid Request : ', JSON.stringify(request, undefined, EditorContext.getTabSize())) + if (!EditorContext.validateRequest(request)) { getLogger().verbose( - `Invalid Request : ${JSON.stringify(request, undefined, EditorContext.getTabSize())}` + 'Invalid Request : ', + JSON.stringify(request, undefined, EditorContext.getTabSize()) ) - errorCode = `Invalid Request` - if (!runtimeLanguageContext.isLanguageSupported(request.fileContext.programmingLanguage.languageName)) { - this.errorMessagePrompt = `${request.fileContext.programmingLanguage.languageName} is currently not supported by CodeWhisperer` + const languageName = request.fileContext.programmingLanguage.languageName + if (!runtimeLanguageContext.isLanguageSupported(languageName)) { + errorMessage = `${languageName} is currently not supported by CodeWhisperer` } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + }) + } + } + + try { + startTime = performance.now() + this.lastInvocationTime = startTime + const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) + const codewhispererPromise = pagination + ? client.listRecommendations(mappedReq) + : client.generateRecommendations(mappedReq) + const resp = await this.getServerResponse( + triggerType, + config.isManualTriggerEnabled, + page === 0, + codewhispererPromise + ) + TelemetryHelper.instance.setSdkApiCallEndTime() + latency = startTime !== 0 ? performance.now() - startTime : 0 + if ('recommendations' in resp) { + recommendations = (resp && resp.recommendations) || [] + } else { + recommendations = (resp && resp.completions) || [] + } + invocationResult = 'Succeeded' + TelemetryHelper.instance.triggerType = triggerType + requestId = resp?.$response && resp?.$response?.requestId + nextToken = resp?.nextToken ? resp?.nextToken : '' + sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] + TelemetryHelper.instance.setFirstResponseRequestId(requestId) + if (page === 0) { + TelemetryHelper.instance.setTimeToFirstRecommendation(performance.now()) + } + if (nextToken === '') { + TelemetryHelper.instance.setLastRequestId(requestId) + TelemetryHelper.instance.setAllPaginationEndTime() } } catch (error) { if (error instanceof CognitoCredentialsError) { @@ -251,88 +250,86 @@ export class RecommendationHandler { } getLogger().error('CodeWhisperer Invocation Exception : %s', (error as Error).message) if (isAwsError(error)) { - this.errorMessagePrompt = error.message + errorMessage = error.message requestId = error.requestId || '' - errorCode = error.code reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}` await this.onThrottlingException(error, triggerType) } else { - errorCode = error as string + errorMessage = error as string reason = error ? String(error) : 'unknown' - this.errorMessagePrompt = errorCode } } finally { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone - getLogger().verbose( - `Request ID: ${requestId}, timestamp(epoch): ${Date.now()}, timezone: ${timezone}, datetime: ${new Date().toLocaleString( - [], - { - timeZone: timezone, - } - )}, vscode version: '${ - vscode.version - }', extension version: '${extensionVersion}', filename: '${EditorContext.getFileName( - editor - )}', left context of line: '${EditorContext.getLeftContext( - editor, - session.startPos.line - )}', line number: ${session.startPos.line}, character location: ${ - session.startPos.character - }, latency: ${latency} ms.` + getLogger().debug( + `Request ID: ${requestId}, + timestamp(epoch): ${Date.now()}, + timezone: ${timezone}, + datetime: ${new Date().toLocaleString([], { timeZone: timezone })}, + vscode version: '${vscode.version}', + extension version: '${extensionVersion}', + filename: '${EditorContext.getFileName(editor)}', + left context of line: '${session.leftContextOfCurrentLine}', + line number: ${session.startPos.line}, + character location: ${session.startPos.character}, + latency: ${latency} ms.` ) getLogger().verbose('Recommendations:') - recommendation.forEach((item, index) => { + recommendations.forEach((item, index) => { getLogger().verbose(`[${index}]\n${item.content.trimRight()}`) }) if (invocationResult === 'Succeeded') { - CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.incrementServiceInvocationCount() + CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() + } + if (shouldRecordServiceInvocation) { + TelemetryHelper.instance.recordServiceInvocationTelemetry( + requestId, + sessionId, + session.recommendations.length + recommendations.length - 1, + triggerType, + autoTriggerType, + invocationResult, + latency, + session.startPos.line, + session.language, + reason, + session.requestContext.supplementalMetadata + ) } } + if (this.isCancellationRequested()) { + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + }) + } + const typedPrefix = editor.document .getText(new vscode.Range(session.startPos, editor.selection.active)) .replace('\r\n', '\n') - if (recommendation.length > 0) { + if (recommendations.length > 0) { TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) // mark suggestions that does not match typeahead when arrival as Discard // these suggestions can be marked as Showed if typeahead can be removed with new inline API - recommendation.forEach((r, i) => { + recommendations.forEach((r, i) => { + const recommendationIndex = i + session.recommendations.length if ( - (!r.content.startsWith(typedPrefix) && - session.getSuggestionState(i + session.recommendations.length) === undefined) || - this.cancellationToken.token.isCancellationRequested + !r.content.startsWith(typedPrefix) && + session.getSuggestionState(recommendationIndex) === undefined ) { - session.setSuggestionState(i + session.recommendations.length, 'Discard') + session.setSuggestionState(recommendationIndex, 'Discard') } - session.setCompletionType(i + session.recommendations.length, r) + session.setCompletionType(recommendationIndex, r) }) - session.recommendations = pagination ? session.recommendations.concat(recommendation) : recommendation - if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(editor, typedPrefix)) { + session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations + if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { this._onDidReceiveRecommendation.fire() } } - if (shouldRecordServiceInvocation) { - TelemetryHelper.instance.recordServiceInvocationTelemetry( - requestId, - sessionId, - session.recommendations.length - 1, - triggerType, - autoTriggerType, - invocationResult, - latency, - session.startPos.line, - languageContext.language, - reason, - session.requestContext.supplementalMetadata - ) - } - if (!this.isCancellationRequested()) { - this.requestId = requestId - session.sessionId = sessionId - this.nextToken = nextToken - this.errorCode = errorCode - } + this.requestId = requestId + session.sessionId = sessionId + this.nextToken = nextToken // send Empty userDecision event if user receives no recommendations in this session at all. if (invocationResult === 'Succeeded' && nextToken === '') { @@ -346,13 +343,17 @@ export class RecommendationHandler { session.requestContext.supplementalMetadata ) } - if (!this.hasAtLeastOneValidSuggestion(editor, typedPrefix)) { - this.reportUserDecisionOfRecommendation(editor, -1) + if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { + this.reportUserDecisions(-1) } } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + }) } - hasAtLeastOneValidSuggestion(editor: vscode.TextEditor, typedPrefix: string): boolean { + hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { return session.recommendations.some(r => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) } @@ -381,19 +382,16 @@ export class RecommendationHandler { session.recommendations = [] session.suggestionStates = new Map() session.completionTypes = new Map() - this.errorCode = '' this.requestId = '' session.sessionId = '' this.nextToken = '' - this.errorMessagePrompt = '' session.requestContext.supplementalMetadata = undefined } - async clearInlineCompletionStates(editor: vscode.TextEditor | undefined) { + async clearInlineCompletionStates() { try { vsCodeState.isCodeWhispererEditing = false - ReferenceInlineProvider.instance.removeInlineReference() - ImportAdderProvider.instance.clear() + application()._clearCodeWhispererUIListener.fire() this.cancelPaginatedRequest() this.clearRecommendations() this.disposeInlineCompletion() @@ -409,10 +407,17 @@ export class RecommendationHandler { } } + reportDiscardedUserDecisions() { + session.recommendations.forEach((r, i) => { + session.setSuggestionState(i, 'Discard') + }) + this.reportUserDecisions(-1) + } + /** * Emits telemetry reflecting user decision for current recommendation. */ - reportUserDecisionOfRecommendation(editor: vscode.TextEditor | undefined, acceptIndex: number) { + reportUserDecisions(acceptIndex: number) { if (session.sessionId === '' || this.requestId === '') { return } @@ -421,7 +426,6 @@ export class RecommendationHandler { session.sessionId, session.recommendations, acceptIndex, - editor?.document.languageId, session.recommendations.length, session.completionTypes, session.suggestionStates, @@ -430,7 +434,7 @@ export class RecommendationHandler { if (isCloud9('any')) { this.clearRecommendations() } else if (isInlineCompletionEnabled()) { - this.clearInlineCompletionStates(editor) + this.clearInlineCompletionStates() } } @@ -438,16 +442,17 @@ export class RecommendationHandler { return this.nextToken !== '' } - canShowRecommendationInIntelliSense(editor: vscode.TextEditor, showPrompt: boolean = false): boolean { + canShowRecommendationInIntelliSense( + editor: vscode.TextEditor, + showPrompt: boolean = false, + response: GetRecommendationsResponse + ): boolean { const reject = () => { - this.reportUserDecisionOfRecommendation(editor, -1) + this.reportUserDecisions(-1) } if (!this.isValidResponse()) { if (showPrompt) { - showTimedMessage( - this.errorMessagePrompt === '' ? CodeWhispererConstants.noSuggestions : this.errorMessagePrompt, - 3000 - ) + showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 3000) } reject() return false @@ -524,6 +529,10 @@ export class RecommendationHandler { async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { await lock.acquire(updateInlineLockKey, async () => { + if (!vscode.window.state.focused) { + this.reportDiscardedUserDecisions() + return + } const inlineCompletionProvider = new CWInlineCompletionItemProvider( this.inlineCompletionProvider?.getActiveItemIndex, indexShift, @@ -555,19 +564,18 @@ export class RecommendationHandler { } async onEditorChange() { - this.reportUserDecisionOfRecommendation(vscode.window.activeTextEditor, -1) + this.reportUserDecisions(-1) } async onFocusChange() { - this.reportUserDecisionOfRecommendation(vscode.window.activeTextEditor, -1) + this.reportUserDecisions(-1) } async onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { // e.kind will be 1 for keyboard cursor change events // we do not want to reset the states for keyboard events because they can be typeahead if (e.kind !== 1 && vscode.window.activeTextEditor === e.textEditor) { - ReferenceInlineProvider.instance.removeInlineReference() - ImportAdderProvider.instance.clear() + application()._clearCodeWhispererUIListener.fire() // when cursor change due to mouse movement we need to reset the active item index for inline if (e.kind === 2) { this.inlineCompletionProvider?.clearActiveItemIndex() @@ -597,7 +605,7 @@ export class RecommendationHandler { session.recommendations.forEach((r, i) => { session.setSuggestionState(i, 'Discard') }) - this.reportUserDecisionOfRecommendation(editor, -1) + this.reportUserDecisions(-1) } else if (session.recommendations.length > 0) { this.subscribeSuggestionCommands() // await this.startRejectionTimer(editor) diff --git a/src/codewhisperer/service/referenceInlineProvider.ts b/src/codewhisperer/service/referenceInlineProvider.ts index 14ac9f7d23..76027dd16d 100644 --- a/src/codewhisperer/service/referenceInlineProvider.ts +++ b/src/codewhisperer/service/referenceInlineProvider.ts @@ -9,6 +9,7 @@ import { getLogger } from '../../shared/logger' import { References } from '../client/codewhisperer' import { LicenseUtil } from '../util/licenseUtil' import { isInlineCompletionEnabled } from '../util/commonUtil' +import { application } from '../util/codeWhispererApplication' const performance = globalThis.performance ?? require('perf_hooks').performance @@ -22,7 +23,11 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter() public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event - constructor() {} + constructor() { + application().clearCodeWhispererUIListener(_ => { + this.removeInlineReference() + }) + } static #instance: ReferenceInlineProvider diff --git a/src/codewhisperer/util/codeWhispererApplication.ts b/src/codewhisperer/util/codeWhispererApplication.ts new file mode 100644 index 0000000000..e9e4947d9e --- /dev/null +++ b/src/codewhisperer/util/codeWhispererApplication.ts @@ -0,0 +1,19 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import vscode from 'vscode' + +class CodeWhispererApplication { + static #instance: CodeWhispererApplication + + readonly _clearCodeWhispererUIListener: vscode.EventEmitter = new vscode.EventEmitter() + public readonly clearCodeWhispererUIListener: vscode.Event = this._clearCodeWhispererUIListener.event + + public static get instance() { + return (this.#instance ??= new CodeWhispererApplication()) + } +} + +export const application = () => CodeWhispererApplication.instance diff --git a/src/codewhisperer/util/codeWhispererSession.ts b/src/codewhisperer/util/codeWhispererSession.ts index 809a291528..cd5ad3206b 100644 --- a/src/codewhisperer/util/codeWhispererSession.ts +++ b/src/codewhisperer/util/codeWhispererSession.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CodewhispererCompletionType } from '../../shared/telemetry/telemetry.gen' +import { CodewhispererCompletionType, CodewhispererLanguage } from '../../shared/telemetry/telemetry.gen' import { GenerateRecommendationsRequest, ListRecommendationsRequest, Recommendation } from '../client/codewhisperer' import { Position } from 'vscode' import { CodeWhispererSupplementalContext } from './supplementalContext/supplementalContextUtil' @@ -16,10 +16,12 @@ class CodeWhispererSession { // Per-session states sessionId = '' startPos = new Position(0, 0) + leftContextOfCurrentLine = '' requestContext: { request: ListRecommendationsRequest | GenerateRecommendationsRequest supplementalMetadata: Omit | undefined } = { request: {} as any, supplementalMetadata: {} as any } + language: CodewhispererLanguage = 'java' // Various states of recommendations recommendations: Recommendation[] = [] diff --git a/src/codewhisperer/util/telemetryHelper.ts b/src/codewhisperer/util/telemetryHelper.ts index 8f4fb82208..50db02cd06 100644 --- a/src/codewhisperer/util/telemetryHelper.ts +++ b/src/codewhisperer/util/telemetryHelper.ts @@ -37,10 +37,7 @@ export class TelemetryHelper { * Trigger type for getting CodeWhisperer recommendation */ public triggerType: CodewhispererTriggerType - /** - * Auto Trigger Type for getting event of Automated Trigger - */ - public CodeWhispererAutomatedtriggerType: CodewhispererAutomatedTriggerType + /** * the cursor offset location at invocation time */ @@ -72,7 +69,6 @@ export class TelemetryHelper { constructor() { this.triggerType = 'OnDemand' - this.CodeWhispererAutomatedtriggerType = 'KeyStrokeCount' this.cursorOffset = 0 } @@ -161,13 +157,11 @@ export class TelemetryHelper { sessionId: string, recommendations: RecommendationsList, acceptIndex: number, - languageId: string | undefined, paginationIndex: number, completionTypes: Map, recommendationSuggestionState?: Map, supplementalContextMetadata?: Omit | undefined ) { - const languageContext = runtimeLanguageContext.getLanguageContext(languageId) const events: CodewhispererUserDecision[] = [] // emit user decision telemetry recommendations.forEach((_elem, i) => { @@ -190,7 +184,7 @@ export class TelemetryHelper { codewhispererSuggestionReferenceCount: _elem.references ? _elem.references.length : 0, codewhispererSuggestionImportCount: getImportCount(_elem), codewhispererCompletionType: this.getCompletionType(i, completionTypes), - codewhispererLanguage: languageContext.language, + codewhispererLanguage: session.language, credentialStartUrl: AuthUtil.instance.startUrl, codewhispererUserGroup: CodeWhispererUserGroupSettings.getUserGroup().toString(), codewhispererSupplementalContextTimeout: supplementalContextMetadata?.isProcessTimeout, @@ -544,7 +538,7 @@ export class TelemetryHelper { // report client component latency after all pagination call finish // and at least one suggestion is shown to the user - public tryRecordClientComponentLatency(languageId: string) { + public tryRecordClientComponentLatency() { if (this.firstSuggestionShowTime === 0 || this.allPaginationEndTime === 0) { return } @@ -559,7 +553,7 @@ export class TelemetryHelper { codewhispererPreprocessingLatency: session.fetchCredentialStartTime - session.invokeSuggestionStartTime, codewhispererCompletionType: 'Line', codewhispererTriggerType: this.triggerType, - codewhispererLanguage: runtimeLanguageContext.getLanguageContext(languageId).language, + codewhispererLanguage: session.language, credentialStartUrl: AuthUtil.instance.startUrl, codewhispererUserGroup: CodeWhispererUserGroupSettings.getUserGroup().toString(), }) diff --git a/src/test/codewhisperer/service/inlineCompletionService.test.ts b/src/test/codewhisperer/service/inlineCompletionService.test.ts index af28b49b44..7539393ee8 100644 --- a/src/test/codewhisperer/service/inlineCompletionService.test.ts +++ b/src/test/codewhisperer/service/inlineCompletionService.test.ts @@ -41,7 +41,10 @@ describe('inlineCompletionService', function () { it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { const mockEditor = createMockTextEditor() - sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves() + sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ + result: 'Succeeded', + errorMessage: undefined, + }) const checkAndResetCancellationTokensStub = sinon.stub( RecommendationHandler.instance, 'checkAndResetCancellationTokens' @@ -73,9 +76,10 @@ describe('inlineCompletionService', function () { ] ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] + session.language = 'python' assert.ok(session.recommendations.length > 0) - await RecommendationHandler.instance.clearInlineCompletionStates(createMockTextEditor()) + await RecommendationHandler.instance.clearInlineCompletionStates() assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) assert.strictEqual(session.recommendations.length, 0) }) diff --git a/src/test/codewhisperer/testUtil.ts b/src/test/codewhisperer/testUtil.ts index 170ff48eb4..9f0f54cb6e 100644 --- a/src/test/codewhisperer/testUtil.ts +++ b/src/test/codewhisperer/testUtil.ts @@ -11,12 +11,14 @@ import { MockDocument } from '../fake/fakeDocument' import { getLogger } from '../../shared/logger' import { CodeWhispererCodeCoverageTracker } from '../../codewhisperer/tracker/codewhispererCodeCoverageTracker' import globals from '../../shared/extensionGlobals' +import { session } from '../../codewhisperer/util/codeWhispererSession' export function resetCodeWhispererGlobalVariables() { vsCodeState.isIntelliSenseActive = false vsCodeState.isCodeWhispererEditing = false CodeWhispererCodeCoverageTracker.instances.clear() globals.telemetry.logger.clear() + session.language = 'python' } export function createMockDocument( diff --git a/src/test/codewhisperer/util/telemetryHelper.test.ts b/src/test/codewhisperer/util/telemetryHelper.test.ts index b7bafa22bc..d473e7fe38 100644 --- a/src/test/codewhisperer/util/telemetryHelper.test.ts +++ b/src/test/codewhisperer/util/telemetryHelper.test.ts @@ -115,6 +115,7 @@ describe('telemetryHelper', function () { let sut: TelemetryHelper beforeEach(function () { + resetCodeWhispererGlobalVariables() sut = new TelemetryHelper() sut.sessionInvocations.push(aServiceInvocation()) CodeWhispererUserGroupSettings.instance.userGroup = CodeWhispererConstants.UserGroup.Control @@ -126,7 +127,6 @@ describe('telemetryHelper', function () { 'aFakeSessionId', [aCompletion(), aCompletion(), aCompletion(), aCompletion()], 0, - 'python', 0, new Map([ [0, 'Line'], @@ -160,7 +160,6 @@ describe('telemetryHelper', function () { 'aFakeSessionId', [aCompletion(), aCompletion(), aCompletion(), aCompletion()], 3, - 'python', 0, new Map([ [0, 'Line'], @@ -194,7 +193,6 @@ describe('telemetryHelper', function () { 'aFakeSessionId', [aCompletion(), aCompletion(), aCompletion(), aCompletion()], -1, - 'python', 0, new Map([ [0, 'Line'], @@ -293,7 +291,6 @@ describe('telemetryHelper', function () { sessionId, response, 0, - 'python', 0, completionTypes, suggestionState